/* * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javafx.scene.control; import javafx.css.PseudoClass; import javafx.scene.control.skin.TreeTableCellSkin; import javafx.beans.InvalidationListener; import javafx.beans.WeakInvalidationListener; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.event.Event; import javafx.collections.WeakListChangeListener; import java.lang.ref.WeakReference; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.scene.AccessibleAction; import javafx.scene.AccessibleAttribute; import javafx.scene.AccessibleRole; import javafx.scene.control.TreeTableColumn.CellEditEvent; import javafx.scene.control.TreeTableView.TreeTableViewFocusModel; /** * Represents a single row/column intersection in a {@link TreeTableView}. To * represent this intersection, a TreeTableCell contains an * {@link #indexProperty() index} property, as well as a * {@link #tableColumnProperty() tableColumn} property. In addition, a TreeTableCell * instance knows what {@link TreeTableRow} it exists in. * *

A note about selection: A TreeTableCell visually shows it is * selected when two conditions are met: *

    *
  1. The {@link TableSelectionModel#isSelected(int, TableColumnBase)} method * returns true for the row / column that this cell represents, and
  2. *
  3. The {@link javafx.scene.control.TableSelectionModel#cellSelectionEnabledProperty() cell selection mode} * property is set to true (to represent that it is allowable to select * individual cells (and not just rows of cells)).
  4. *
* * @param The type of the TableView generic type * @see TreeTableView * @see TreeTableColumn * @see Cell * @see IndexedCell * @see TreeTableRow * @param The type of the item contained within the Cell. * @since JavaFX 8.0 */ public class TreeTableCell extends IndexedCell { /*************************************************************************** * * * Constructors * * * **************************************************************************/ /** * Constructs a default TreeTableCell instance with a style class of * 'tree-table-cell'. */ public TreeTableCell() { getStyleClass().addAll(DEFAULT_STYLE_CLASS); setAccessibleRole(AccessibleRole.TREE_TABLE_CELL); updateColumnIndex(); } /*************************************************************************** * * * Private fields * * * **************************************************************************/ // package for testing boolean lockItemOnEdit = false; /*************************************************************************** * * * Callbacks and Events * * * **************************************************************************/ private boolean itemDirty = false; /* * This is the list observer we use to keep an eye on the SelectedCells * ObservableList in the tree table view. Because it is possible that the table can * be mutated, we create this observer here, and add/remove it from the * storeTableView method. */ private ListChangeListener> selectedListener = c -> { while (c.next()) { if (c.wasAdded() || c.wasRemoved()) { updateSelection(); } } }; // same as above, but for focus private final InvalidationListener focusedListener = value -> { updateFocus(); }; // same as above, but for for changes to the properties on TableRow private final InvalidationListener tableRowUpdateObserver = value -> { itemDirty = true; requestLayout(); }; private final InvalidationListener editingListener = value -> { updateEditing(); }; private ListChangeListener> visibleLeafColumnsListener = c -> { updateColumnIndex(); }; private ListChangeListener columnStyleClassListener = c -> { while (c.next()) { if (c.wasRemoved()) { getStyleClass().removeAll(c.getRemoved()); } if (c.wasAdded()) { getStyleClass().addAll(c.getAddedSubList()); } } }; private final InvalidationListener rootPropertyListener = observable -> { updateItem(-1); }; private final InvalidationListener columnStyleListener = value -> { if (getTableColumn() != null) { possiblySetStyle(getTableColumn().getStyle()); } }; private final InvalidationListener columnIdListener = value -> { if (getTableColumn() != null) { possiblySetId(getTableColumn().getId()); } }; private final WeakListChangeListener> weakSelectedListener = new WeakListChangeListener>(selectedListener); private final WeakInvalidationListener weakFocusedListener = new WeakInvalidationListener(focusedListener); private final WeakInvalidationListener weaktableRowUpdateObserver = new WeakInvalidationListener(tableRowUpdateObserver); private final WeakInvalidationListener weakEditingListener = new WeakInvalidationListener(editingListener); private final WeakListChangeListener> weakVisibleLeafColumnsListener = new WeakListChangeListener>(visibleLeafColumnsListener); private final WeakListChangeListener weakColumnStyleClassListener = new WeakListChangeListener(columnStyleClassListener); private final WeakInvalidationListener weakColumnStyleListener = new WeakInvalidationListener(columnStyleListener); private final WeakInvalidationListener weakColumnIdListener = new WeakInvalidationListener(columnIdListener); private final WeakInvalidationListener weakRootPropertyListener = new WeakInvalidationListener(rootPropertyListener); /*************************************************************************** * * * Properties * * * **************************************************************************/ // --- TableColumn /** * The TreeTableColumn instance that backs this TreeTableCell. */ private ReadOnlyObjectWrapper> treeTableColumn = new ReadOnlyObjectWrapper>(this, "treeTableColumn") { @Override protected void invalidated() { updateColumnIndex(); } }; public final ReadOnlyObjectProperty> tableColumnProperty() { return treeTableColumn.getReadOnlyProperty(); } private void setTableColumn(TreeTableColumn value) { treeTableColumn.set(value); } public final TreeTableColumn getTableColumn() { return treeTableColumn.get(); } // --- TableView /** * The TreeTableView associated with this TreeTableCell. */ private ReadOnlyObjectWrapper> treeTableView; private void setTreeTableView(TreeTableView value) { treeTableViewPropertyImpl().set(value); } public final TreeTableView getTreeTableView() { return treeTableView == null ? null : treeTableView.get(); } public final ReadOnlyObjectProperty> treeTableViewProperty() { return treeTableViewPropertyImpl().getReadOnlyProperty(); } private ReadOnlyObjectWrapper> treeTableViewPropertyImpl() { if (treeTableView == null) { treeTableView = new ReadOnlyObjectWrapper>(this, "treeTableView") { private WeakReference> weakTableViewRef; @Override protected void invalidated() { TreeTableView.TreeTableViewSelectionModel sm; TreeTableView.TreeTableViewFocusModel fm; if (weakTableViewRef != null) { TreeTableView oldTableView = weakTableViewRef.get(); if (oldTableView != null) { sm = oldTableView.getSelectionModel(); if (sm != null) { sm.getSelectedCells().removeListener(weakSelectedListener); } fm = oldTableView.getFocusModel(); if (fm != null) { fm.focusedCellProperty().removeListener(weakFocusedListener); } oldTableView.editingCellProperty().removeListener(weakEditingListener); oldTableView.getVisibleLeafColumns().removeListener(weakVisibleLeafColumnsListener); oldTableView.rootProperty().removeListener(weakRootPropertyListener); } } TreeTableView newTreeTableView = get(); if (newTreeTableView != null) { sm = newTreeTableView.getSelectionModel(); if (sm != null) { sm.getSelectedCells().addListener(weakSelectedListener); } fm = newTreeTableView.getFocusModel(); if (fm != null) { fm.focusedCellProperty().addListener(weakFocusedListener); } newTreeTableView.editingCellProperty().addListener(weakEditingListener); newTreeTableView.getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener); newTreeTableView.rootProperty().addListener(weakRootPropertyListener); weakTableViewRef = new WeakReference>(newTreeTableView); } updateColumnIndex(); } }; } return treeTableView; } // --- TableRow /** * The TreeTableRow that this TreeTableCell currently finds itself placed within. */ private ReadOnlyObjectWrapper> treeTableRow = new ReadOnlyObjectWrapper>(this, "treeTableRow"); private void setTreeTableRow(TreeTableRow value) { treeTableRow.set(value); } public final TreeTableRow getTreeTableRow() { return treeTableRow.get(); } public final ReadOnlyObjectProperty> tableRowProperty() { return treeTableRow; } /*************************************************************************** * * * Editing API * * * **************************************************************************/ /** {@inheritDoc} */ @Override public void startEdit() { if (isEditing()) return; final TreeTableView table = getTreeTableView(); final TreeTableColumn column = getTableColumn(); if (! isEditable() || (table != null && ! table.isEditable()) || (column != null && ! getTableColumn().isEditable())) { return; } // We check the boolean lockItemOnEdit field here, as whilst we want to // updateItem normally, when it comes to unit tests we can't have the // item change in all circumstances. if (! lockItemOnEdit) { updateItem(-1); } // it makes sense to get the cell into its editing state before firing // the event to listeners below, so that's what we're doing here // by calling super.startEdit(). super.startEdit(); if (column != null) { CellEditEvent editEvent = new CellEditEvent( table, table.getEditingCell(), TreeTableColumn.editStartEvent(), null ); Event.fireEvent(column, editEvent); } } /** {@inheritDoc} */ @Override public void commitEdit(T newValue) { if (! isEditing()) return; final TreeTableView table = getTreeTableView(); if (table != null) { @SuppressWarnings("unchecked") TreeTablePosition editingCell = (TreeTablePosition) table.getEditingCell(); // Inform the TableView of the edit being ready to be committed. CellEditEvent editEvent = new CellEditEvent( table, editingCell, TreeTableColumn.editCommitEvent(), newValue ); Event.fireEvent(getTableColumn(), editEvent); } // inform parent classes of the commit, so that they can switch us // out of the editing state. // This MUST come before the updateItem call below, otherwise it will // call cancelEdit(), resulting in both commit and cancel events being // fired (as identified in RT-29650) super.commitEdit(newValue); // update the item within this cell, so that it represents the new value updateItem(newValue, false); if (table != null) { // reset the editing cell on the TableView table.edit(-1, null); // request focus back onto the table, only if the current focus // owner has the table as a parent (otherwise the user might have // clicked out of the table entirely and given focus to something else. // It would be rude of us to request it back again. ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(table); } } /** {@inheritDoc} */ @Override public void cancelEdit() { if (! isEditing()) return; final TreeTableView table = getTreeTableView(); super.cancelEdit(); // reset the editing index on the TableView if (table != null) { @SuppressWarnings("unchecked") TreeTablePosition editingCell = (TreeTablePosition) table.getEditingCell(); if (updateEditingIndex) table.edit(-1, null); // request focus back onto the table, only if the current focus // owner has the table as a parent (otherwise the user might have // clicked out of the table entirely and given focus to something else. // It would be rude of us to request it back again. ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(table); CellEditEvent editEvent = new CellEditEvent( table, editingCell, TreeTableColumn.editCancelEvent(), null ); Event.fireEvent(getTableColumn(), editEvent); } } /* ************************************************************************* * * * Overriding methods * * * **************************************************************************/ /** {@inheritDoc} */ @Override public void updateSelected(boolean selected) { // copied from Cell, with the first conditional clause below commented // out, as it is valid for an empty TableCell to be selected, as long // as the parent TableRow is not empty (see RT-15529). /*if (selected && isEmpty()) return;*/ if (getTreeTableRow() == null || getTreeTableRow().isEmpty()) return; setSelected(selected); } /* ************************************************************************* * * * Private Implementation * * * **************************************************************************/ /** {@inheritDoc} */ @Override void indexChanged(int oldIndex, int newIndex) { super.indexChanged(oldIndex, newIndex); if (isEditing() && newIndex == oldIndex) { // no-op // Fix for RT-31165 - if we (needlessly) update the index whilst the // cell is being edited it will no longer be in an editing state. // This means that in certain (common) circumstances that it will // appear that a cell is uneditable as, despite being clicked, it // will not change to the editing state as a layout of VirtualFlow // is immediately invoked, which forces all cells to be updated. } else { // Ideally we would just use the following two lines of code, rather // than the updateItem() call beneath, but if we do this we end up with // RT-22428 where all the columns are collapsed. // itemDirty = true; // requestLayout(); updateItem(oldIndex); updateSelection(); updateFocus(); updateEditing(); } } private boolean isLastVisibleColumn = false; private int columnIndex = -1; private void updateColumnIndex() { final TreeTableView tv = getTreeTableView(); TreeTableColumn tc = getTableColumn(); columnIndex = tv == null || tc == null ? -1 : tv.getVisibleLeafIndex(tc); // update the pseudo class state regarding whether this is the last // visible cell (i.e. the right-most). isLastVisibleColumn = getTableColumn() != null && columnIndex != -1 && columnIndex == tv.getVisibleLeafColumns().size() - 1; pseudoClassStateChanged(PSEUDO_CLASS_LAST_VISIBLE, isLastVisibleColumn); } private void updateSelection() { /* * This cell should be selected if the selection mode of the table * is cell-based, and if the row and column that this cell represents * is selected. * * If the selection mode is not cell-based, then the listener in the * TableRow class might pick up the need to set an entire row to be * selected. */ if (isEmpty()) return; final boolean isSelected = isSelected(); if (! isInCellSelectionMode()) { if (isSelected) { updateSelected(false); } return; } final TreeTableView tv = getTreeTableView(); if (getIndex() == -1 || tv == null) return; TreeTableView.TreeTableViewSelectionModel sm = tv.getSelectionModel(); if (sm == null) { updateSelected(false); return; } boolean isSelectedNow = sm.isSelected(getIndex(), getTableColumn()); if (isSelected == isSelectedNow) return; updateSelected(isSelectedNow); } private void updateFocus() { final boolean isFocused = isFocused(); if (! isInCellSelectionMode()) { if (isFocused) { setFocused(false); } return; } final TreeTableView tv = getTreeTableView(); if (getIndex() == -1 || tv == null) return; TreeTableView.TreeTableViewFocusModel fm = tv.getFocusModel(); if (fm == null) { setFocused(false); return; } setFocused(fm.isFocused(getIndex(), getTableColumn())); } private void updateEditing() { final TreeTableView tv = getTreeTableView(); if (getIndex() == -1 || tv == null) return; TreeTablePosition editCell = tv.getEditingCell(); boolean match = match(editCell); if (match && ! isEditing()) { startEdit(); } else if (! match && isEditing()) { // If my index is not the one being edited then I need to cancel // the edit. The tricky thing here is that as part of this call // I cannot end up calling list.edit(-1) the way that the standard // cancelEdit method would do. Yet, I need to call cancelEdit // so that subclasses which override cancelEdit can execute. So, // I have to use a kind of hacky flag workaround. updateEditingIndex = false; cancelEdit(); updateEditingIndex = true; } } private boolean updateEditingIndex = true; private boolean match(TreeTablePosition pos) { return pos != null && pos.getRow() == getIndex() && pos.getTableColumn() == getTableColumn(); } private boolean isInCellSelectionMode() { TreeTableView tv = getTreeTableView(); if (tv == null) return false; TreeTableView.TreeTableViewSelectionModel sm = tv.getSelectionModel(); return sm != null && sm.isCellSelectionEnabled(); } /* * This was brought in to fix the issue in RT-22077, namely that the * ObservableValue was being GC'd, meaning that changes to the value were * no longer being delivered. By extracting this value out of the method, * it is now referred to from TableCell and will therefore no longer be * GC'd. */ private ObservableValue currentObservableValue = null; private boolean isFirstRun = true; private WeakReference oldRowItemRef; /* * This is called when we think that the data within this TreeTableCell may have * changed. You'll note that this is a private function - it is only called * when one of the triggers above call it. */ private void updateItem(int oldIndex) { if (currentObservableValue != null) { currentObservableValue.removeListener(weaktableRowUpdateObserver); } // get the total number of items in the data model final TreeTableView tableView = getTreeTableView(); final TreeTableColumn tableColumn = getTableColumn(); final int itemCount = tableView == null ? -1 : getTreeTableView().getExpandedItemCount(); final int index = getIndex(); final boolean isEmpty = isEmpty(); final T oldValue = getItem(); final TreeTableRow tableRow = getTreeTableRow(); final S rowItem = tableRow == null ? null : tableRow.getItem(); final boolean indexExceedsItemCount = index >= itemCount; // there is a whole heap of reasons why we should just punt... outer: if (indexExceedsItemCount || index < 0 || columnIndex < 0 || !isVisible() || tableColumn == null || !tableColumn.isVisible() || tableView.getRoot() == null) { // RT-30484 We need to allow a first run to be special-cased to allow // for the updateItem method to be called at least once to allow for // the correct visual state to be set up. In particular, in RT-30484 // refer to Ensemble8PopUpTree.png - in this case the arrows are being // shown as the new cells are instantiated with the arrows in the // children list, and are only hidden in updateItem. // RT-32621: There are circumstances where we need to updateItem, // even when the index is greater than the itemCount. For example, // RT-32621 identifies issues where a TreeTableView collapses a // TreeItem but the custom cells remain visible. This is now // resolved with the check for indexExceedsItemCount. if ((!isEmpty && oldValue != null) || isFirstRun || indexExceedsItemCount) { updateItem(null, true); isFirstRun = false; } return; } else { currentObservableValue = tableColumn.getCellObservableValue(index); final T newValue = currentObservableValue == null ? null : currentObservableValue.getValue(); // RT-35864 - if the index didn't change, then avoid calling updateItem // unless the item has changed. if (oldIndex == index) { if (!isItemChanged(oldValue, newValue)) { // RT-36670: we need to check the row item here to prevent // the issue where the cell value and index doesn't change, // but the backing row object does. S oldRowItem = oldRowItemRef != null ? oldRowItemRef.get() : null; if (oldRowItem != null && oldRowItem.equals(rowItem)) { // RT-37054: we break out of the if/else code here and // proceed with the code following this, so that we may // still update references, listeners, etc as required. break outer; } } } updateItem(newValue, false); } oldRowItemRef = new WeakReference<>(rowItem); if (currentObservableValue == null) { return; } // add property change listeners to this item currentObservableValue.addListener(weaktableRowUpdateObserver); } @Override protected void layoutChildren() { if (itemDirty) { updateItem(-1); itemDirty = false; } super.layoutChildren(); } /*************************************************************************** * * * Expert API * * * **************************************************************************/ /** * Updates the TreeTableView associated with this TreeTableCell. This is typically * only done once when the TreeTableCell is first added to the TreeTableView. * * Note: This function is intended to be used by experts, primarily * by those implementing new Skins. It is not common * for developers or designers to access this function directly. * @param tv the TreeTableView associated with this TreeTableCell */ public final void updateTreeTableView(TreeTableView tv) { setTreeTableView(tv); } /** * Updates the TreeTableRow associated with this TreeTableCell. * * Note: This function is intended to be used by experts, primarily * by those implementing new Skins. It is not common * for developers or designers to access this function directly. * @param treeTableRow the TreeTableRow associated with this TreeTableCell */ public final void updateTreeTableRow(TreeTableRow treeTableRow) { this.setTreeTableRow(treeTableRow); } /** * Updates the TreeTableColumn associated with this TreeTableCell. * * Note: This function is intended to be used by experts, primarily * by those implementing new Skins. It is not common * for developers or designers to access this function directly. * @param col the TreeTableColumn associated with this TreeTableCell */ public final void updateTreeTableColumn(TreeTableColumn col) { // remove style class of existing tree table column, if it is non-null TreeTableColumn oldCol = getTableColumn(); if (oldCol != null) { oldCol.getStyleClass().removeListener(weakColumnStyleClassListener); getStyleClass().removeAll(oldCol.getStyleClass()); oldCol.idProperty().removeListener(weakColumnIdListener); oldCol.styleProperty().removeListener(weakColumnStyleListener); String id = getId(); String style = getStyle(); if (id != null && id.equals(oldCol.getId())) { setId(null); } if (style != null && style.equals(oldCol.getStyle())) { setStyle(""); } } setTableColumn(col); if (col != null) { getStyleClass().addAll(col.getStyleClass()); col.getStyleClass().addListener(weakColumnStyleClassListener); col.idProperty().addListener(weakColumnIdListener); col.styleProperty().addListener(weakColumnStyleListener); possiblySetId(col.getId()); possiblySetStyle(col.getStyle()); } } /*************************************************************************** * * * Stylesheet Handling * * * **************************************************************************/ private static final String DEFAULT_STYLE_CLASS = "tree-table-cell"; private static final PseudoClass PSEUDO_CLASS_LAST_VISIBLE = PseudoClass.getPseudoClass("last-visible"); /** {@inheritDoc} */ @Override protected Skin createDefaultSkin() { return new TreeTableCellSkin(this); } private void possiblySetId(String idCandidate) { if (getId() == null || getId().isEmpty()) { setId(idCandidate); } } private void possiblySetStyle(String styleCandidate) { if (getStyle() == null || getStyle().isEmpty()) { setStyle(styleCandidate); } } /*************************************************************************** * * * Accessibility handling * * * **************************************************************************/ @Override public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { switch (attribute) { case ROW_INDEX: return getIndex(); case COLUMN_INDEX: return columnIndex; case SELECTED: return isInCellSelectionMode() ? isSelected() : getTreeTableRow().isSelected(); default: return super.queryAccessibleAttribute(attribute, parameters); } } @Override public void executeAccessibleAction(AccessibleAction action, Object... parameters) { switch (action) { case REQUEST_FOCUS: { TreeTableView treeTableView = getTreeTableView(); if (treeTableView != null) { TreeTableViewFocusModel fm = treeTableView.getFocusModel(); if (fm != null) { fm.focus(getIndex(), getTableColumn()); } } break; } default: super.executeAccessibleAction(action, parameters); } } }