1 /*
   2  * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control;
  27 
  28 import javafx.css.PseudoClass;
  29 import javafx.scene.control.skin.TreeTableCellSkin;
  30 import javafx.beans.InvalidationListener;
  31 import javafx.beans.WeakInvalidationListener;
  32 import javafx.beans.value.ObservableValue;
  33 import javafx.collections.ListChangeListener;
  34 import javafx.event.Event;
  35 
  36 import javafx.collections.WeakListChangeListener;
  37 import java.lang.ref.WeakReference;
  38 import javafx.beans.property.ReadOnlyObjectProperty;
  39 import javafx.beans.property.ReadOnlyObjectWrapper;
  40 
  41 import javafx.scene.AccessibleAction;
  42 import javafx.scene.AccessibleAttribute;
  43 import javafx.scene.AccessibleRole;
  44 import javafx.scene.control.TreeTableColumn.CellEditEvent;
  45 import javafx.scene.control.TreeTableView.TreeTableViewFocusModel;
  46 
  47 
  48 /**
  49  * Represents a single row/column intersection in a {@link TreeTableView}. To
  50  * represent this intersection, a TreeTableCell contains an
  51  * {@link #indexProperty() index} property, as well as a
  52  * {@link #tableColumnProperty() tableColumn} property. In addition, a TreeTableCell
  53  * instance knows what {@link TreeTableRow} it exists in.
  54  *
  55  * <p><strong>A note about selection:</strong> A TreeTableCell visually shows it is
  56  * selected when two conditions are met:
  57  * <ol>
  58  *   <li>The {@link TableSelectionModel#isSelected(int, TableColumnBase)} method
  59  *   returns true for the row / column that this cell represents, and</li>
  60  *   <li>The {@link javafx.scene.control.TableSelectionModel#cellSelectionEnabledProperty() cell selection mode}
  61  *   property is set to true (to represent that it is allowable to select
  62  *   individual cells (and not just rows of cells)).</li>
  63  * </ol>
  64  * </p>
  65  *
  66  * @see TreeTableView
  67  * @see TreeTableColumn
  68  * @see Cell
  69  * @see IndexedCell
  70  * @see TreeTableRow
  71  * @param <T> The type of the item contained within the Cell.
  72  * @since JavaFX 8.0
  73  */
  74 public class TreeTableCell<S,T> extends IndexedCell<T> {
  75 
  76     /***************************************************************************
  77      *                                                                         *
  78      * Constructors                                                            *
  79      *                                                                         *
  80      **************************************************************************/
  81 
  82     /**
  83      * Constructs a default TreeTableCell instance with a style class of
  84      * 'tree-table-cell'.
  85      */
  86     public TreeTableCell() {
  87         getStyleClass().addAll(DEFAULT_STYLE_CLASS);
  88         setAccessibleRole(AccessibleRole.TREE_TABLE_CELL);
  89 
  90         updateColumnIndex();
  91     }
  92 
  93 
  94 
  95     /***************************************************************************
  96      *                                                                         *
  97      * Private fields                                                          *
  98      *                                                                         *
  99      **************************************************************************/
 100 
 101     // package for testing
 102     boolean lockItemOnEdit = false;
 103 
 104 
 105 
 106     /***************************************************************************
 107      *                                                                         *
 108      * Callbacks and Events                                                    *
 109      *                                                                         *
 110      **************************************************************************/
 111 
 112     private boolean itemDirty = false;
 113 
 114     /*
 115      * This is the list observer we use to keep an eye on the SelectedCells
 116      * ObservableList in the tree table view. Because it is possible that the table can
 117      * be mutated, we create this observer here, and add/remove it from the
 118      * storeTableView method.
 119      */
 120     private ListChangeListener<TreeTablePosition<S,?>> selectedListener = c -> {
 121         while (c.next()) {
 122             if (c.wasAdded() || c.wasRemoved()) {
 123                 updateSelection();
 124             }
 125         }
 126     };
 127 
 128     // same as above, but for focus
 129     private final InvalidationListener focusedListener = value -> {
 130         updateFocus();
 131     };
 132 
 133     // same as above, but for for changes to the properties on TableRow
 134     private final InvalidationListener tableRowUpdateObserver = value -> {
 135         itemDirty = true;
 136         requestLayout();
 137     };
 138 
 139     private final InvalidationListener editingListener = value -> {
 140         updateEditing();
 141     };
 142 
 143     private ListChangeListener<TreeTableColumn<S,?>> visibleLeafColumnsListener = c -> {
 144         updateColumnIndex();
 145     };
 146 
 147     private ListChangeListener<String> columnStyleClassListener = c -> {
 148         while (c.next()) {
 149             if (c.wasRemoved()) {
 150                 getStyleClass().removeAll(c.getRemoved());
 151             }
 152 
 153             if (c.wasAdded()) {
 154                 getStyleClass().addAll(c.getAddedSubList());
 155             }
 156         }
 157     };
 158 
 159     private final InvalidationListener rootPropertyListener = observable -> {
 160         updateItem(-1);
 161     };
 162 
 163     private final InvalidationListener columnStyleListener = value -> {
 164         if (getTableColumn() != null) {
 165             possiblySetStyle(getTableColumn().getStyle());
 166         }
 167     };
 168 
 169     private final InvalidationListener columnIdListener = value -> {
 170         if (getTableColumn() != null) {
 171             possiblySetId(getTableColumn().getId());
 172         }
 173     };
 174 
 175     private final WeakListChangeListener<TreeTablePosition<S,?>> weakSelectedListener =
 176             new WeakListChangeListener<TreeTablePosition<S,?>>(selectedListener);
 177     private final WeakInvalidationListener weakFocusedListener =
 178             new WeakInvalidationListener(focusedListener);
 179     private final WeakInvalidationListener weaktableRowUpdateObserver =
 180             new WeakInvalidationListener(tableRowUpdateObserver);
 181     private final WeakInvalidationListener weakEditingListener =
 182             new WeakInvalidationListener(editingListener);
 183     private final WeakListChangeListener<TreeTableColumn<S,?>> weakVisibleLeafColumnsListener =
 184             new WeakListChangeListener<TreeTableColumn<S,?>>(visibleLeafColumnsListener);
 185     private final WeakListChangeListener<String> weakColumnStyleClassListener =
 186             new WeakListChangeListener<String>(columnStyleClassListener);
 187     private final WeakInvalidationListener weakColumnStyleListener =
 188             new WeakInvalidationListener(columnStyleListener);
 189     private final WeakInvalidationListener weakColumnIdListener =
 190             new WeakInvalidationListener(columnIdListener);
 191     private final WeakInvalidationListener weakRootPropertyListener =
 192             new WeakInvalidationListener(rootPropertyListener);
 193 
 194 
 195     /***************************************************************************
 196      *                                                                         *
 197      * Properties                                                              *
 198      *                                                                         *
 199      **************************************************************************/
 200 
 201     // --- TableColumn
 202     /**
 203      * The TreeTableColumn instance that backs this TreeTableCell.
 204      */
 205     private ReadOnlyObjectWrapper<TreeTableColumn<S,T>> treeTableColumn =
 206             new ReadOnlyObjectWrapper<TreeTableColumn<S,T>>(this, "treeTableColumn") {
 207         @Override protected void invalidated() {
 208             updateColumnIndex();
 209         }
 210     };
 211     public final ReadOnlyObjectProperty<TreeTableColumn<S,T>> tableColumnProperty() { return treeTableColumn.getReadOnlyProperty(); }
 212     private void setTableColumn(TreeTableColumn<S,T> value) { treeTableColumn.set(value); }
 213     public final TreeTableColumn<S,T> getTableColumn() { return treeTableColumn.get(); }
 214 
 215 
 216     // --- TableView
 217     /**
 218      * The TreeTableView associated with this TreeTableCell.
 219      */
 220     private ReadOnlyObjectWrapper<TreeTableView<S>> treeTableView;
 221     private void setTreeTableView(TreeTableView<S> value) {
 222         treeTableViewPropertyImpl().set(value);
 223     }
 224     public final TreeTableView<S> getTreeTableView() {
 225         return treeTableView == null ? null : treeTableView.get();
 226     }
 227     public final ReadOnlyObjectProperty<TreeTableView<S>> treeTableViewProperty() {
 228         return treeTableViewPropertyImpl().getReadOnlyProperty();
 229     }
 230 
 231     private ReadOnlyObjectWrapper<TreeTableView<S>> treeTableViewPropertyImpl() {
 232         if (treeTableView == null) {
 233             treeTableView = new ReadOnlyObjectWrapper<TreeTableView<S>>(this, "treeTableView") {
 234                 private WeakReference<TreeTableView<S>> weakTableViewRef;
 235                 @Override protected void invalidated() {
 236                     TreeTableView.TreeTableViewSelectionModel<S> sm;
 237                     TreeTableView.TreeTableViewFocusModel<S> fm;
 238 
 239                     if (weakTableViewRef != null) {
 240                         TreeTableView<S> oldTableView = weakTableViewRef.get();
 241                         if (oldTableView != null) {
 242                             sm = oldTableView.getSelectionModel();
 243                             if (sm != null) {
 244                                 sm.getSelectedCells().removeListener(weakSelectedListener);
 245                             }
 246 
 247                             fm = oldTableView.getFocusModel();
 248                             if (fm != null) {
 249                                 fm.focusedCellProperty().removeListener(weakFocusedListener);
 250                             }
 251 
 252                             oldTableView.editingCellProperty().removeListener(weakEditingListener);
 253                             oldTableView.getVisibleLeafColumns().removeListener(weakVisibleLeafColumnsListener);
 254                             oldTableView.rootProperty().removeListener(weakRootPropertyListener);
 255                         }
 256                     }
 257 
 258                     TreeTableView<S> newTreeTableView = get();
 259                     if (newTreeTableView != null) {
 260                         sm = newTreeTableView.getSelectionModel();
 261                         if (sm != null) {
 262                             sm.getSelectedCells().addListener(weakSelectedListener);
 263                         }
 264 
 265                         fm = newTreeTableView.getFocusModel();
 266                         if (fm != null) {
 267                             fm.focusedCellProperty().addListener(weakFocusedListener);
 268                         }
 269 
 270                         newTreeTableView.editingCellProperty().addListener(weakEditingListener);
 271                         newTreeTableView.getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener);
 272                         newTreeTableView.rootProperty().addListener(weakRootPropertyListener);
 273 
 274                         weakTableViewRef = new WeakReference<TreeTableView<S>>(newTreeTableView);
 275                     }
 276 
 277                     updateColumnIndex();
 278                 }
 279             };
 280         }
 281         return treeTableView;
 282     }
 283 
 284 
 285     // --- TableRow
 286     /**
 287      * The TreeTableRow that this TreeTableCell currently finds itself placed within.
 288      */
 289     private ReadOnlyObjectWrapper<TreeTableRow<S>> treeTableRow =
 290             new ReadOnlyObjectWrapper<TreeTableRow<S>>(this, "treeTableRow");
 291     private void setTreeTableRow(TreeTableRow<S> value) { treeTableRow.set(value); }
 292     public final TreeTableRow<S> getTreeTableRow() { return treeTableRow.get(); }
 293     public final ReadOnlyObjectProperty<TreeTableRow<S>> tableRowProperty() { return treeTableRow;  }
 294 
 295 
 296 
 297     /***************************************************************************
 298      *                                                                         *
 299      * Editing API                                                             *
 300      *                                                                         *
 301      **************************************************************************/
 302 
 303     /** {@inheritDoc} */
 304     @Override public void startEdit() {
 305         if (isEditing()) return;
 306 
 307         final TreeTableView<S> table = getTreeTableView();
 308         final TreeTableColumn<S,T> column = getTableColumn();
 309         if (! isEditable() ||
 310                 (table != null && ! table.isEditable()) ||
 311                 (column != null && ! getTableColumn().isEditable())) {
 312             return;
 313         }
 314 
 315         // We check the boolean lockItemOnEdit field here, as whilst we want to
 316         // updateItem normally, when it comes to unit tests we can't have the
 317         // item change in all circumstances.
 318         if (! lockItemOnEdit) {
 319             updateItem(-1);
 320         }
 321 
 322         // it makes sense to get the cell into its editing state before firing
 323         // the event to listeners below, so that's what we're doing here
 324         // by calling super.startEdit().
 325         super.startEdit();
 326 
 327         if (column != null) {
 328             CellEditEvent editEvent = new CellEditEvent(
 329                 table,
 330                 table.getEditingCell(),
 331                 TreeTableColumn.<S,T>editStartEvent(),
 332                 null
 333             );
 334 
 335             Event.fireEvent(column, editEvent);
 336         }
 337     }
 338 
 339     /** {@inheritDoc} */
 340     @Override public void commitEdit(T newValue) {
 341         if (! isEditing()) return;
 342 
 343         final TreeTableView<S> table = getTreeTableView();
 344         if (table != null) {
 345             @SuppressWarnings("unchecked")
 346             TreeTablePosition<S,T> editingCell = (TreeTablePosition<S,T>) table.getEditingCell();
 347 
 348             // Inform the TableView of the edit being ready to be committed.
 349             CellEditEvent<S,T> editEvent = new CellEditEvent<S,T>(
 350                 table,
 351                 editingCell,
 352                 TreeTableColumn.<S,T>editCommitEvent(),
 353                 newValue
 354             );
 355 
 356             Event.fireEvent(getTableColumn(), editEvent);
 357         }
 358 
 359         // inform parent classes of the commit, so that they can switch us
 360         // out of the editing state.
 361         // This MUST come before the updateItem call below, otherwise it will
 362         // call cancelEdit(), resulting in both commit and cancel events being
 363         // fired (as identified in RT-29650)
 364         super.commitEdit(newValue);
 365 
 366         // update the item within this cell, so that it represents the new value
 367         updateItem(newValue, false);
 368 
 369         if (table != null) {
 370             // reset the editing cell on the TableView
 371             table.edit(-1, null);
 372 
 373             // request focus back onto the table, only if the current focus
 374             // owner has the table as a parent (otherwise the user might have
 375             // clicked out of the table entirely and given focus to something else.
 376             // It would be rude of us to request it back again.
 377             ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(table);
 378         }
 379     }
 380 
 381     /** {@inheritDoc} */
 382     @Override public void cancelEdit() {
 383         if (! isEditing()) return;
 384 
 385         final TreeTableView<S> table = getTreeTableView();
 386 
 387         super.cancelEdit();
 388 
 389         // reset the editing index on the TableView
 390         if (table != null) {
 391             @SuppressWarnings("unchecked")
 392             TreeTablePosition<S,T> editingCell = (TreeTablePosition<S,T>) table.getEditingCell();
 393 
 394             if (updateEditingIndex) table.edit(-1, null);
 395 
 396             // request focus back onto the table, only if the current focus
 397             // owner has the table as a parent (otherwise the user might have
 398             // clicked out of the table entirely and given focus to something else.
 399             // It would be rude of us to request it back again.
 400             ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(table);
 401 
 402             CellEditEvent<S,T> editEvent = new CellEditEvent<S,T>(
 403                 table,
 404                 editingCell,
 405                 TreeTableColumn.<S,T>editCancelEvent(),
 406                 null
 407             );
 408 
 409             Event.fireEvent(getTableColumn(), editEvent);
 410         }
 411     }
 412 
 413 
 414 
 415     /* *************************************************************************
 416      *                                                                         *
 417      * Overriding methods                                                      *
 418      *                                                                         *
 419      **************************************************************************/
 420 
 421     /** {@inheritDoc} */
 422     @Override public void updateSelected(boolean selected) {
 423         // copied from Cell, with the first conditional clause below commented
 424         // out, as it is valid for an empty TableCell to be selected, as long
 425         // as the parent TableRow is not empty (see RT-15529).
 426         /*if (selected && isEmpty()) return;*/
 427         if (getTreeTableRow() == null || getTreeTableRow().isEmpty()) return;
 428         setSelected(selected);
 429     }
 430 
 431 
 432 
 433     /* *************************************************************************
 434      *                                                                         *
 435      * Private Implementation                                                  *
 436      *                                                                         *
 437      **************************************************************************/
 438 
 439     /** {@inheritDoc} */
 440     @Override void indexChanged(int oldIndex, int newIndex) {
 441         super.indexChanged(oldIndex, newIndex);
 442 
 443         if (isEditing() && newIndex == oldIndex) {
 444             // no-op
 445             // Fix for RT-31165 - if we (needlessly) update the index whilst the
 446             // cell is being edited it will no longer be in an editing state.
 447             // This means that in certain (common) circumstances that it will
 448             // appear that a cell is uneditable as, despite being clicked, it
 449             // will not change to the editing state as a layout of VirtualFlow
 450             // is immediately invoked, which forces all cells to be updated.
 451         } else {
 452             // Ideally we would just use the following two lines of code, rather
 453             // than the updateItem() call beneath, but if we do this we end up with
 454             // RT-22428 where all the columns are collapsed.
 455             // itemDirty = true;
 456             // requestLayout();
 457             updateItem(oldIndex);
 458             updateSelection();
 459             updateFocus();
 460             updateEditing();
 461         }
 462     }
 463 
 464     private boolean isLastVisibleColumn = false;
 465     private int columnIndex = -1;
 466 
 467     private void updateColumnIndex() {
 468         final TreeTableView<S> tv = getTreeTableView();
 469         TreeTableColumn<S,T> tc = getTableColumn();
 470         columnIndex = tv == null || tc == null ? -1 : tv.getVisibleLeafIndex(tc);
 471 
 472         // update the pseudo class state regarding whether this is the last
 473         // visible cell (i.e. the right-most).
 474         isLastVisibleColumn = getTableColumn() != null &&
 475                 columnIndex != -1 &&
 476                 columnIndex == tv.getVisibleLeafColumns().size() - 1;
 477         pseudoClassStateChanged(PSEUDO_CLASS_LAST_VISIBLE, isLastVisibleColumn);
 478     }
 479 
 480     private void updateSelection() {
 481         /*
 482          * This cell should be selected if the selection mode of the table
 483          * is cell-based, and if the row and column that this cell represents
 484          * is selected.
 485          *
 486          * If the selection mode is not cell-based, then the listener in the
 487          * TableRow class might pick up the need to set an entire row to be
 488          * selected.
 489          */
 490         if (isEmpty()) return;
 491 
 492         final boolean isSelected = isSelected();
 493         if (! isInCellSelectionMode()) {
 494             if (isSelected) {
 495                 updateSelected(false);
 496             }
 497             return;
 498         }
 499 
 500         final TreeTableView<S> tv = getTreeTableView();
 501         if (getIndex() == -1 || tv == null) return;
 502 
 503         TreeTableView.TreeTableViewSelectionModel<S> sm = tv.getSelectionModel();
 504         if (sm == null) {
 505             updateSelected(false);
 506             return;
 507         }
 508 
 509         boolean isSelectedNow = sm.isSelected(getIndex(), getTableColumn());
 510         if (isSelected == isSelectedNow) return;
 511 
 512         updateSelected(isSelectedNow);
 513     }
 514 
 515     private void updateFocus() {
 516         final boolean isFocused = isFocused();
 517         if (! isInCellSelectionMode()) {
 518             if (isFocused) {
 519                 setFocused(false);
 520             }
 521             return;
 522         }
 523 
 524         final TreeTableView<S> tv = getTreeTableView();
 525         if (getIndex() == -1 || tv == null) return;
 526 
 527         TreeTableView.TreeTableViewFocusModel<S> fm = tv.getFocusModel();
 528         if (fm == null) {
 529             setFocused(false);
 530             return;
 531         }
 532 
 533         setFocused(fm.isFocused(getIndex(), getTableColumn()));
 534     }
 535 
 536     private void updateEditing() {
 537         final TreeTableView<S> tv = getTreeTableView();
 538         if (getIndex() == -1 || tv == null) return;
 539 
 540         TreeTablePosition<S,?> editCell = tv.getEditingCell();
 541         boolean match = match(editCell);
 542 
 543         if (match && ! isEditing()) {
 544             startEdit();
 545         } else if (! match && isEditing()) {
 546             // If my index is not the one being edited then I need to cancel
 547             // the edit. The tricky thing here is that as part of this call
 548             // I cannot end up calling list.edit(-1) the way that the standard
 549             // cancelEdit method would do. Yet, I need to call cancelEdit
 550             // so that subclasses which override cancelEdit can execute. So,
 551             // I have to use a kind of hacky flag workaround.
 552             updateEditingIndex = false;
 553             cancelEdit();
 554             updateEditingIndex = true;
 555         }
 556     }
 557     private boolean updateEditingIndex = true;
 558 
 559     private boolean match(TreeTablePosition pos) {
 560         return pos != null && pos.getRow() == getIndex() && pos.getTableColumn() == getTableColumn();
 561     }
 562 
 563     private boolean isInCellSelectionMode() {
 564         TreeTableView<S> tv = getTreeTableView();
 565         if (tv == null) return false;
 566         TreeTableView.TreeTableViewSelectionModel<S> sm = tv.getSelectionModel();
 567         return sm != null && sm.isCellSelectionEnabled();
 568     }
 569 
 570     /*
 571      * This was brought in to fix the issue in RT-22077, namely that the
 572      * ObservableValue was being GC'd, meaning that changes to the value were
 573      * no longer being delivered. By extracting this value out of the method,
 574      * it is now referred to from TableCell and will therefore no longer be
 575      * GC'd.
 576      */
 577     private ObservableValue<T> currentObservableValue = null;
 578 
 579     private boolean isFirstRun = true;
 580 
 581     private WeakReference<S> oldRowItemRef;
 582 
 583     /*
 584      * This is called when we think that the data within this TreeTableCell may have
 585      * changed. You'll note that this is a private function - it is only called
 586      * when one of the triggers above call it.
 587      */
 588     private void updateItem(int oldIndex) {
 589         if (currentObservableValue != null) {
 590             currentObservableValue.removeListener(weaktableRowUpdateObserver);
 591         }
 592 
 593         // get the total number of items in the data model
 594         final TreeTableView<S> tableView = getTreeTableView();
 595         final TreeTableColumn<S,T> tableColumn = getTableColumn();
 596         final int itemCount = tableView == null ? -1 : getTreeTableView().getExpandedItemCount();
 597         final int index = getIndex();
 598         final boolean isEmpty = isEmpty();
 599         final T oldValue = getItem();
 600 
 601         final TreeTableRow<S> tableRow = getTreeTableRow();
 602         final S rowItem = tableRow == null ? null : tableRow.getItem();
 603 
 604         final boolean indexExceedsItemCount = index >= itemCount;
 605 
 606         // there is a whole heap of reasons why we should just punt...
 607         outer: if (indexExceedsItemCount ||
 608                 index < 0 ||
 609                 columnIndex < 0 ||
 610                 !isVisible() ||
 611                 tableColumn == null ||
 612                 !tableColumn.isVisible() ||
 613                 tableView.getRoot() == null) {
 614 
 615             // RT-30484 We need to allow a first run to be special-cased to allow
 616             // for the updateItem method to be called at least once to allow for
 617             // the correct visual state to be set up. In particular, in RT-30484
 618             // refer to Ensemble8PopUpTree.png - in this case the arrows are being
 619             // shown as the new cells are instantiated with the arrows in the
 620             // children list, and are only hidden in updateItem.
 621             // RT-32621: There are circumstances where we need to updateItem,
 622             // even when the index is greater than the itemCount. For example,
 623             // RT-32621 identifies issues where a TreeTableView collapses a
 624             // TreeItem but the custom cells remain visible. This is now
 625             // resolved with the check for indexExceedsItemCount.
 626             if ((!isEmpty && oldValue != null) || isFirstRun || indexExceedsItemCount) {
 627                 updateItem(null, true);
 628                 isFirstRun = false;
 629             }
 630             return;
 631         } else {
 632             currentObservableValue = tableColumn.getCellObservableValue(index);
 633 
 634             final T newValue = currentObservableValue == null ? null : currentObservableValue.getValue();
 635 
 636             // RT-35864 - if the index didn't change, then avoid calling updateItem
 637             // unless the item has changed.
 638             if (oldIndex == index) {
 639                 if (!isItemChanged(oldValue, newValue)) {
 640                     // RT-36670: we need to check the row item here to prevent
 641                     // the issue where the cell value and index doesn't change,
 642                     // but the backing row object does.
 643                     S oldRowItem = oldRowItemRef != null ? oldRowItemRef.get() : null;
 644                     if (oldRowItem != null && oldRowItem.equals(rowItem)) {
 645                         // RT-37054:  we break out of the if/else code here and
 646                         // proceed with the code following this, so that we may
 647                         // still update references, listeners, etc as required.
 648                         break outer;
 649                     }
 650                 }
 651             }
 652             updateItem(newValue, false);
 653         }
 654 
 655         oldRowItemRef = new WeakReference<>(rowItem);
 656 
 657         if (currentObservableValue == null) {
 658             return;
 659         }
 660 
 661         // add property change listeners to this item
 662         currentObservableValue.addListener(weaktableRowUpdateObserver);
 663     }
 664 
 665     @Override protected void layoutChildren() {
 666         if (itemDirty) {
 667             updateItem(-1);
 668             itemDirty = false;
 669         }
 670         super.layoutChildren();
 671     }
 672 
 673 
 674 
 675 
 676     /***************************************************************************
 677      *                                                                         *
 678      *                              Expert API                                 *
 679      *                                                                         *
 680      **************************************************************************/
 681 
 682     /**
 683      * Updates the TreeTableView associated with this TreeTableCell. This is typically
 684      * only done once when the TreeTableCell is first added to the TreeTableView.
 685      *
 686      * @expert This function is intended to be used by experts, primarily
 687      *         by those implementing new Skins. It is not common
 688      *         for developers or designers to access this function directly.
 689      */
 690     public final void updateTreeTableView(TreeTableView<S> tv) {
 691         setTreeTableView(tv);
 692     }
 693 
 694     /**
 695      * Updates the TreeTableRow associated with this TreeTableCell.
 696      *
 697      * @expert This function is intended to be used by experts, primarily
 698      *         by those implementing new Skins. It is not common
 699      *         for developers or designers to access this function directly.
 700      */
 701     public final void updateTreeTableRow(TreeTableRow<S> treeTableRow) {
 702         this.setTreeTableRow(treeTableRow);
 703     }
 704 
 705     /**
 706      * Updates the TreeTableColumn associated with this TreeTableCell.
 707      *
 708      * @expert This function is intended to be used by experts, primarily
 709      *         by those implementing new Skins. It is not common
 710      *         for developers or designers to access this function directly.
 711      */
 712     public final void updateTreeTableColumn(TreeTableColumn<S,T> col) {
 713         // remove style class of existing tree table column, if it is non-null
 714         TreeTableColumn<S,T> oldCol = getTableColumn();
 715         if (oldCol != null) {
 716             oldCol.getStyleClass().removeListener(weakColumnStyleClassListener);
 717             getStyleClass().removeAll(oldCol.getStyleClass());
 718 
 719             oldCol.idProperty().removeListener(weakColumnIdListener);
 720             oldCol.styleProperty().removeListener(weakColumnStyleListener);
 721 
 722             String id = getId();
 723             String style = getStyle();
 724             if (id != null && id.equals(oldCol.getId())) {
 725                 setId(null);
 726             }
 727             if (style != null && style.equals(oldCol.getStyle())) {
 728                 setStyle("");
 729             }
 730         }
 731 
 732         setTableColumn(col);
 733 
 734         if (col != null) {
 735             getStyleClass().addAll(col.getStyleClass());
 736             col.getStyleClass().addListener(weakColumnStyleClassListener);
 737 
 738             col.idProperty().addListener(weakColumnIdListener);
 739             col.styleProperty().addListener(weakColumnStyleListener);
 740 
 741             possiblySetId(col.getId());
 742             possiblySetStyle(col.getStyle());
 743         }
 744     }
 745 
 746 
 747 
 748     /***************************************************************************
 749      *                                                                         *
 750      * Stylesheet Handling                                                     *
 751      *                                                                         *
 752      **************************************************************************/
 753 
 754     private static final String DEFAULT_STYLE_CLASS = "tree-table-cell";
 755     private static final PseudoClass PSEUDO_CLASS_LAST_VISIBLE =
 756             PseudoClass.getPseudoClass("last-visible");
 757 
 758     /** {@inheritDoc} */
 759     @Override protected Skin<?> createDefaultSkin() {
 760         return new TreeTableCellSkin<S,T>(this);
 761     }
 762 
 763     private void possiblySetId(String idCandidate) {
 764         if (getId() == null || getId().isEmpty()) {
 765             setId(idCandidate);
 766         }
 767     }
 768 
 769     private void possiblySetStyle(String styleCandidate) {
 770         if (getStyle() == null || getStyle().isEmpty()) {
 771             setStyle(styleCandidate);
 772         }
 773     }
 774 
 775 
 776     /***************************************************************************
 777      *                                                                         *
 778      * Accessibility handling                                                  *
 779      *                                                                         *
 780      **************************************************************************/
 781 
 782     @Override
 783     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 784         switch (attribute) {
 785             case ROW_INDEX: return getIndex();
 786             case COLUMN_INDEX: return columnIndex;
 787             case SELECTED: return isInCellSelectionMode() ? isSelected() : getTreeTableRow().isSelected();
 788             default: return super.queryAccessibleAttribute(attribute, parameters);
 789         }
 790     }
 791 
 792     @Override
 793     public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
 794         switch (action) {
 795             case REQUEST_FOCUS: {
 796                 TreeTableView<S> treeTableView = getTreeTableView();
 797                 if (treeTableView != null) {
 798                     TreeTableViewFocusModel<S> fm = treeTableView.getFocusModel();
 799                     if (fm != null) {
 800                         fm.focus(getIndex(), getTableColumn());
 801                     }
 802                 }
 803                 break;
 804             }
 805             default: super.executeAccessibleAction(action, parameters);
 806         }
 807     }
 808 }