1 /*
   2  * Copyright (c) 2010, 2017, 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  *
  65  * @param <S> The type of the TreeTableView generic type
  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 
 345         // inform parent classes of the commit, so that they can switch us
 346         // out of the editing state.
 347         // This MUST come before the updateItem call below, otherwise it will
 348         // call cancelEdit(), resulting in both commit and cancel events being
 349         // fired (as identified in RT-29650)
 350         super.commitEdit(newValue);
 351 
 352         if (table != null) {
 353             @SuppressWarnings("unchecked")
 354             // Inform the TreeTableView of the edit being ready to be committed.
 355                     CellEditEvent<S,T> editEvent = new CellEditEvent<S,T>(
 356                     table,
 357                     new TreeTablePosition<>(getTreeTableView(), getIndex(), getTableColumn()),
 358                     TreeTableColumn.<S,T>editCommitEvent(),
 359                     newValue
 360             );
 361 
 362             Event.fireEvent(getTableColumn(), editEvent);
 363         }
 364 
 365         // update the item within this cell, so that it represents the new value
 366         updateItem(newValue, false);
 367 
 368         if (table != null) {
 369             // reset the editing cell on the TableView
 370             table.edit(-1, null);
 371 
 372             // request focus back onto the table, only if the current focus
 373             // owner has the table as a parent (otherwise the user might have
 374             // clicked out of the table entirely and given focus to something else.
 375             // It would be rude of us to request it back again.
 376             ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(table);
 377         }
 378     }
 379 
 380     /** {@inheritDoc} */
 381     @Override public void cancelEdit() {
 382         if (! isEditing()) return;
 383 
 384         final TreeTableView<S> table = getTreeTableView();
 385 
 386         super.cancelEdit();
 387 
 388         // reset the editing index on the TableView
 389         if (table != null) {
 390             @SuppressWarnings("unchecked")
 391             TreeTablePosition<S,T> editingCell = (TreeTablePosition<S,T>) table.getEditingCell();
 392 
 393             if (updateEditingIndex) table.edit(-1, null);
 394 
 395             // request focus back onto the table, only if the current focus
 396             // owner has the table as a parent (otherwise the user might have
 397             // clicked out of the table entirely and given focus to something else.
 398             // It would be rude of us to request it back again.
 399             ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(table);
 400 
 401             CellEditEvent<S,T> editEvent = new CellEditEvent<S,T>(
 402                 table,
 403                 editingCell,
 404                 TreeTableColumn.<S,T>editCancelEvent(),
 405                 null
 406             );
 407 
 408             Event.fireEvent(getTableColumn(), editEvent);
 409         }
 410     }
 411 
 412 
 413 
 414     /* *************************************************************************
 415      *                                                                         *
 416      * Overriding methods                                                      *
 417      *                                                                         *
 418      **************************************************************************/
 419 
 420     /** {@inheritDoc} */
 421     @Override public void updateSelected(boolean selected) {
 422         // copied from Cell, with the first conditional clause below commented
 423         // out, as it is valid for an empty TableCell to be selected, as long
 424         // as the parent TableRow is not empty (see RT-15529).
 425         /*if (selected && isEmpty()) return;*/
 426         if (getTreeTableRow() == null || getTreeTableRow().isEmpty()) return;
 427         setSelected(selected);
 428     }
 429 
 430 
 431 
 432     /* *************************************************************************
 433      *                                                                         *
 434      * Private Implementation                                                  *
 435      *                                                                         *
 436      **************************************************************************/
 437 
 438     /** {@inheritDoc} */
 439     @Override void indexChanged(int oldIndex, int newIndex) {
 440         super.indexChanged(oldIndex, newIndex);
 441 
 442         if (isEditing()) {
 443             // no-op
 444             // Fix for RT-31165 - if we (needlessly) update the index whilst the
 445             // cell is being edited it will no longer be in an editing state.
 446             // This means that in certain (common) circumstances that it will
 447             // appear that a cell is uneditable as, despite being clicked, it
 448             // will not change to the editing state as a layout of VirtualFlow
 449             // is immediately invoked, which forces all cells to be updated.
 450         } else {
 451             // Ideally we would just use the following two lines of code, rather
 452             // than the updateItem() call beneath, but if we do this we end up with
 453             // RT-22428 where all the columns are collapsed.
 454             // itemDirty = true;
 455             // requestLayout();
 456             updateItem(oldIndex);
 457             updateEditing();
 458         }
 459 
 460         updateSelection();
 461         updateFocus();
 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             attemptEditCommit();
 547         }
 548     }
 549     private boolean updateEditingIndex = true;
 550 
 551     private boolean match(TreeTablePosition pos) {
 552         return pos != null && pos.getRow() == getIndex() && pos.getTableColumn() == getTableColumn();
 553     }
 554 
 555     private boolean isInCellSelectionMode() {
 556         TreeTableView<S> tv = getTreeTableView();
 557         if (tv == null) return false;
 558         TreeTableView.TreeTableViewSelectionModel<S> sm = tv.getSelectionModel();
 559         return sm != null && sm.isCellSelectionEnabled();
 560     }
 561 
 562     /*
 563      * This was brought in to fix the issue in RT-22077, namely that the
 564      * ObservableValue was being GC'd, meaning that changes to the value were
 565      * no longer being delivered. By extracting this value out of the method,
 566      * it is now referred to from TableCell and will therefore no longer be
 567      * GC'd.
 568      */
 569     private ObservableValue<T> currentObservableValue = null;
 570 
 571     private boolean isFirstRun = true;
 572 
 573     private WeakReference<S> oldRowItemRef;
 574 
 575     /*
 576      * This is called when we think that the data within this TreeTableCell may have
 577      * changed. You'll note that this is a private function - it is only called
 578      * when one of the triggers above call it.
 579      */
 580     private void updateItem(int oldIndex) {
 581         if (currentObservableValue != null) {
 582             currentObservableValue.removeListener(weaktableRowUpdateObserver);
 583         }
 584 
 585         // get the total number of items in the data model
 586         final TreeTableView<S> tableView = getTreeTableView();
 587         final TreeTableColumn<S,T> tableColumn = getTableColumn();
 588         final int itemCount = tableView == null ? -1 : getTreeTableView().getExpandedItemCount();
 589         final int index = getIndex();
 590         final boolean isEmpty = isEmpty();
 591         final T oldValue = getItem();
 592 
 593         final TreeTableRow<S> tableRow = getTreeTableRow();
 594         final S rowItem = tableRow == null ? null : tableRow.getItem();
 595 
 596         final boolean indexExceedsItemCount = index >= itemCount;
 597 
 598         // there is a whole heap of reasons why we should just punt...
 599         outer: if (indexExceedsItemCount ||
 600                 index < 0 ||
 601                 columnIndex < 0 ||
 602                 !isVisible() ||
 603                 tableColumn == null ||
 604                 !tableColumn.isVisible() ||
 605                 tableView.getRoot() == null) {
 606 
 607             // RT-30484 We need to allow a first run to be special-cased to allow
 608             // for the updateItem method to be called at least once to allow for
 609             // the correct visual state to be set up. In particular, in RT-30484
 610             // refer to Ensemble8PopUpTree.png - in this case the arrows are being
 611             // shown as the new cells are instantiated with the arrows in the
 612             // children list, and are only hidden in updateItem.
 613             // RT-32621: There are circumstances where we need to updateItem,
 614             // even when the index is greater than the itemCount. For example,
 615             // RT-32621 identifies issues where a TreeTableView collapses a
 616             // TreeItem but the custom cells remain visible. This is now
 617             // resolved with the check for indexExceedsItemCount.
 618             if ((!isEmpty && oldValue != null) || isFirstRun || indexExceedsItemCount) {
 619                 updateItem(null, true);
 620                 isFirstRun = false;
 621             }
 622             return;
 623         } else {
 624             currentObservableValue = tableColumn.getCellObservableValue(index);
 625 
 626             final T newValue = currentObservableValue == null ? null : currentObservableValue.getValue();
 627 
 628             // RT-35864 - if the index didn't change, then avoid calling updateItem
 629             // unless the item has changed.
 630             if (oldIndex == index) {
 631                 if (!isItemChanged(oldValue, newValue)) {
 632                     // RT-36670: we need to check the row item here to prevent
 633                     // the issue where the cell value and index doesn't change,
 634                     // but the backing row object does.
 635                     S oldRowItem = oldRowItemRef != null ? oldRowItemRef.get() : null;
 636                     if (oldRowItem != null && oldRowItem.equals(rowItem)) {
 637                         // RT-37054:  we break out of the if/else code here and
 638                         // proceed with the code following this, so that we may
 639                         // still update references, listeners, etc as required.
 640                         break outer;
 641                     }
 642                 }
 643             }
 644             updateItem(newValue, false);
 645         }
 646 
 647         oldRowItemRef = new WeakReference<>(rowItem);
 648 
 649         if (currentObservableValue == null) {
 650             return;
 651         }
 652 
 653         // add property change listeners to this item
 654         currentObservableValue.addListener(weaktableRowUpdateObserver);
 655     }
 656 
 657     @Override protected void layoutChildren() {
 658         if (itemDirty) {
 659             updateItem(-1);
 660             itemDirty = false;
 661         }
 662         super.layoutChildren();
 663     }
 664 
 665 
 666 
 667 
 668     /***************************************************************************
 669      *                                                                         *
 670      *                              Expert API                                 *
 671      *                                                                         *
 672      **************************************************************************/
 673 
 674     /**
 675      * Updates the TreeTableView associated with this TreeTableCell. This is typically
 676      * only done once when the TreeTableCell is first added to the TreeTableView.
 677      *
 678      * Note: This function is intended to be used by experts, primarily
 679      *       by those implementing new Skins. It is not common
 680      *       for developers or designers to access this function directly.
 681      * @param tv the TreeTableView associated with this TreeTableCell
 682      */
 683     public final void updateTreeTableView(TreeTableView<S> tv) {
 684         setTreeTableView(tv);
 685     }
 686 
 687     /**
 688      * Updates the TreeTableRow associated with this TreeTableCell.
 689      *
 690      * Note: This function is intended to be used by experts, primarily
 691      *       by those implementing new Skins. It is not common
 692      *       for developers or designers to access this function directly.
 693      * @param treeTableRow the TreeTableRow associated with this TreeTableCell
 694      */
 695     public final void updateTreeTableRow(TreeTableRow<S> treeTableRow) {
 696         this.setTreeTableRow(treeTableRow);
 697     }
 698 
 699     /**
 700      * Updates the TreeTableColumn associated with this TreeTableCell.
 701      *
 702      * Note: This function is intended to be used by experts, primarily
 703      *       by those implementing new Skins. It is not common
 704      *       for developers or designers to access this function directly.
 705      * @param col the TreeTableColumn associated with this TreeTableCell
 706      */
 707     public final void updateTreeTableColumn(TreeTableColumn<S,T> col) {
 708         // remove style class of existing tree table column, if it is non-null
 709         TreeTableColumn<S,T> oldCol = getTableColumn();
 710         if (oldCol != null) {
 711             oldCol.getStyleClass().removeListener(weakColumnStyleClassListener);
 712             getStyleClass().removeAll(oldCol.getStyleClass());
 713 
 714             oldCol.idProperty().removeListener(weakColumnIdListener);
 715             oldCol.styleProperty().removeListener(weakColumnStyleListener);
 716 
 717             String id = getId();
 718             String style = getStyle();
 719             if (id != null && id.equals(oldCol.getId())) {
 720                 setId(null);
 721             }
 722             if (style != null && style.equals(oldCol.getStyle())) {
 723                 setStyle("");
 724             }
 725         }
 726 
 727         setTableColumn(col);
 728 
 729         if (col != null) {
 730             getStyleClass().addAll(col.getStyleClass());
 731             col.getStyleClass().addListener(weakColumnStyleClassListener);
 732 
 733             col.idProperty().addListener(weakColumnIdListener);
 734             col.styleProperty().addListener(weakColumnStyleListener);
 735 
 736             possiblySetId(col.getId());
 737             possiblySetStyle(col.getStyle());
 738         }
 739     }
 740 
 741 
 742 
 743     /***************************************************************************
 744      *                                                                         *
 745      * Stylesheet Handling                                                     *
 746      *                                                                         *
 747      **************************************************************************/
 748 
 749     private static final String DEFAULT_STYLE_CLASS = "tree-table-cell";
 750     private static final PseudoClass PSEUDO_CLASS_LAST_VISIBLE =
 751             PseudoClass.getPseudoClass("last-visible");
 752 
 753     /** {@inheritDoc} */
 754     @Override protected Skin<?> createDefaultSkin() {
 755         return new TreeTableCellSkin<S,T>(this);
 756     }
 757 
 758     private void possiblySetId(String idCandidate) {
 759         if (getId() == null || getId().isEmpty()) {
 760             setId(idCandidate);
 761         }
 762     }
 763 
 764     private void possiblySetStyle(String styleCandidate) {
 765         if (getStyle() == null || getStyle().isEmpty()) {
 766             setStyle(styleCandidate);
 767         }
 768     }
 769 
 770 
 771     /***************************************************************************
 772      *                                                                         *
 773      * Accessibility handling                                                  *
 774      *                                                                         *
 775      **************************************************************************/
 776 
 777     @Override
 778     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 779         switch (attribute) {
 780             case ROW_INDEX: return getIndex();
 781             case COLUMN_INDEX: return columnIndex;
 782             case SELECTED: return isInCellSelectionMode() ? isSelected() : getTreeTableRow().isSelected();
 783             default: return super.queryAccessibleAttribute(attribute, parameters);
 784         }
 785     }
 786 
 787     @Override
 788     public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
 789         switch (action) {
 790             case REQUEST_FOCUS: {
 791                 TreeTableView<S> treeTableView = getTreeTableView();
 792                 if (treeTableView != null) {
 793                     TreeTableViewFocusModel<S> fm = treeTableView.getFocusModel();
 794                     if (fm != null) {
 795                         fm.focus(getIndex(), getTableColumn());
 796                     }
 797                 }
 798                 break;
 799             }
 800             default: super.executeAccessibleAction(action, parameters);
 801         }
 802     }
 803 }