1 /*
   2  * Copyright (c) 2012, 2015, 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.TreeTableRowSkin;
  30 import java.lang.ref.WeakReference;
  31 
  32 import javafx.beans.InvalidationListener;
  33 import javafx.beans.Observable;
  34 import javafx.beans.WeakInvalidationListener;
  35 import javafx.beans.property.BooleanProperty;
  36 import javafx.beans.property.ObjectProperty;
  37 import javafx.beans.property.ReadOnlyObjectProperty;
  38 import javafx.beans.property.ReadOnlyObjectWrapper;
  39 import javafx.beans.property.SimpleObjectProperty;
  40 import javafx.collections.ListChangeListener;
  41 import javafx.collections.WeakListChangeListener;
  42 import javafx.scene.AccessibleAction;
  43 import javafx.scene.AccessibleAttribute;
  44 import javafx.scene.AccessibleRole;
  45 import javafx.scene.Node;
  46 import javafx.scene.control.TreeTableView.TreeTableViewFocusModel;
  47 import javafx.scene.control.TreeTableView.TreeTableViewSelectionModel;
  48 
  49 /**
  50  * <p>TreeTableRow is an {@link javafx.scene.control.IndexedCell IndexedCell}, but
  51  * rarely needs to be used by developers creating TreeTableView instances. The only
  52  * time TreeTableRow is likely to be encountered at all by a developer is if they
  53  * wish to create a custom {@link TreeTableView#rowFactoryProperty() rowFactory}
  54  * that replaces an entire row of a TreeTableView.</p>
  55  *
  56  * <p>More often than not, it is actually easier for a developer to customize
  57  * individual cells in a row, rather than the whole row itself. To do this,
  58  * you can specify a custom {@link TreeTableColumn#cellFactoryProperty() cellFactory}
  59  * on each TreeTableColumn instance.</p>
  60  *
  61  * @see TreeTableView
  62  * @see TreeTableColumn
  63  * @see TreeTableCell
  64  * @see IndexedCell
  65  * @see Cell
  66  * @param <T> The type of the item contained within the Cell.
  67  * @since JavaFX 8.0
  68  */
  69 public class TreeTableRow<T> extends IndexedCell<T> {
  70 
  71 
  72     /***************************************************************************
  73      *                                                                         *
  74      * Constructors                                                            *
  75      *                                                                         *
  76      **************************************************************************/
  77 
  78     /**
  79      * Creates a default TreeTableRow instance.
  80      */
  81     public TreeTableRow() {
  82         getStyleClass().addAll(DEFAULT_STYLE_CLASS);
  83         setAccessibleRole(AccessibleRole.TREE_TABLE_ROW);
  84     }
  85 
  86 
  87 
  88     /***************************************************************************
  89      *                                                                         *
  90      * Callbacks and events                                                    *
  91      *                                                                         *
  92      **************************************************************************/
  93 
  94     private final ListChangeListener<Integer> selectedListener = c -> {
  95         updateSelection();
  96     };
  97 
  98     private final InvalidationListener focusedListener = valueModel -> {
  99         updateFocus();
 100     };
 101 
 102     private final InvalidationListener editingListener = valueModel -> {
 103         updateEditing();
 104     };
 105 
 106     private final InvalidationListener leafListener = new InvalidationListener() {
 107         @Override public void invalidated(Observable valueModel) {
 108             // necessary to update the disclosure node in the skin when the
 109             // leaf property changes
 110             TreeItem<T> treeItem = getTreeItem();
 111             if (treeItem != null) {
 112                 requestLayout();
 113             }
 114         }
 115     };
 116 
 117     private boolean oldExpanded;
 118     private final InvalidationListener treeItemExpandedInvalidationListener = o -> {
 119         final boolean expanded = ((BooleanProperty)o).get();
 120         pseudoClassStateChanged(EXPANDED_PSEUDOCLASS_STATE,   expanded);
 121         pseudoClassStateChanged(COLLAPSED_PSEUDOCLASS_STATE, !expanded);
 122         if (expanded != oldExpanded) {
 123             notifyAccessibleAttributeChanged(AccessibleAttribute.EXPANDED);
 124         }
 125         oldExpanded = expanded;
 126     };
 127 
 128     private final WeakListChangeListener<Integer> weakSelectedListener =
 129             new WeakListChangeListener<Integer>(selectedListener);
 130     private final WeakInvalidationListener weakFocusedListener =
 131             new WeakInvalidationListener(focusedListener);
 132     private final WeakInvalidationListener weakEditingListener =
 133             new WeakInvalidationListener(editingListener);
 134     private final WeakInvalidationListener weakLeafListener =
 135             new WeakInvalidationListener(leafListener);
 136     private final WeakInvalidationListener weakTreeItemExpandedInvalidationListener =
 137             new WeakInvalidationListener(treeItemExpandedInvalidationListener);
 138 
 139 
 140 
 141     /***************************************************************************
 142      *                                                                         *
 143      * Properties                                                              *
 144      *                                                                         *
 145      **************************************************************************/
 146 
 147     // --- TreeItem
 148     private ReadOnlyObjectWrapper<TreeItem<T>> treeItem =
 149         new ReadOnlyObjectWrapper<TreeItem<T>>(this, "treeItem") {
 150 
 151             TreeItem<T> oldValue = null;
 152 
 153             @Override protected void invalidated() {
 154                 if (oldValue != null) {
 155                     oldValue.expandedProperty().removeListener(weakTreeItemExpandedInvalidationListener);
 156                 }
 157 
 158                 oldValue = get();
 159 
 160                 if (oldValue != null) {
 161                     oldExpanded = oldValue.isExpanded();
 162                     oldValue.expandedProperty().addListener(weakTreeItemExpandedInvalidationListener);
 163                     // fake an invalidation to ensure updated pseudo-class state
 164                     weakTreeItemExpandedInvalidationListener.invalidated(oldValue.expandedProperty());
 165                 }
 166             }
 167     };
 168     private void setTreeItem(TreeItem<T> value) {
 169         treeItem.set(value);
 170     }
 171 
 172     /**
 173      * Returns the TreeItem currently set in this TreeCell.
 174      * @return the TreeItem currently set in this TreeCell
 175      */
 176     public final TreeItem<T> getTreeItem() { return treeItem.get(); }
 177 
 178     /**
 179      * Each TreeTableCell represents at most a single {@link TreeItem}, which is
 180      * represented by this property.
 181      * @return the tree item property
 182      */
 183     public final ReadOnlyObjectProperty<TreeItem<T>> treeItemProperty() { return treeItem.getReadOnlyProperty(); }
 184 
 185 
 186 
 187     // --- Disclosure Node
 188     private ObjectProperty<Node> disclosureNode = new SimpleObjectProperty<Node>(this, "disclosureNode");
 189 
 190     /**
 191      * The node to use as the "disclosure" triangle, or toggle, used for
 192      * expanding and collapsing items. This is only used in the case of
 193      * an item in the tree which contains child items. If not specified, the
 194      * TreeTableCell's Skin implementation is responsible for providing a default
 195      * disclosure node.
 196      * @param value the disclosure node
 197      */
 198     public final void setDisclosureNode(Node value) { disclosureNodeProperty().set(value); }
 199 
 200     /**
 201      * Returns the current disclosure node set in this TreeTableCell.
 202      * @return the disclosure node
 203      */
 204     public final Node getDisclosureNode() { return disclosureNode.get(); }
 205 
 206     /**
 207      * The disclosure node is commonly seen represented as a triangle that rotates
 208      * on screen to indicate whether or not the TreeItem that it is placed
 209      * beside is expanded or collapsed.
 210      * @return the disclosure node property
 211      */
 212     public final ObjectProperty<Node> disclosureNodeProperty() { return disclosureNode; }
 213 
 214 
 215     // --- TreeView
 216     private ReadOnlyObjectWrapper<TreeTableView<T>> treeTableView = new ReadOnlyObjectWrapper<TreeTableView<T>>(this, "treeTableView") {
 217         private WeakReference<TreeTableView<T>> weakTreeTableViewRef;
 218         @Override protected void invalidated() {
 219             TreeTableViewSelectionModel<T> sm;
 220             TreeTableViewFocusModel<T> fm;
 221 
 222             if (weakTreeTableViewRef != null) {
 223                 TreeTableView<T> oldTreeTableView = weakTreeTableViewRef.get();
 224                 if (oldTreeTableView != null) {
 225                     // remove old listeners
 226                     sm = oldTreeTableView.getSelectionModel();
 227                     if (sm != null) {
 228                         sm.getSelectedIndices().removeListener(weakSelectedListener);
 229                     }
 230 
 231                     fm = oldTreeTableView.getFocusModel();
 232                     if (fm != null) {
 233                         fm.focusedIndexProperty().removeListener(weakFocusedListener);
 234                     }
 235 
 236                     oldTreeTableView.editingCellProperty().removeListener(weakEditingListener);
 237                 }
 238 
 239                 weakTreeTableViewRef = null;
 240             }
 241 
 242             if (get() != null) {
 243                 sm = get().getSelectionModel();
 244                 if (sm != null) {
 245                     // listening for changes to treeView.selectedIndex and IndexedCell.index,
 246                     // to determine if this cell is selected
 247                     sm.getSelectedIndices().addListener(weakSelectedListener);
 248                 }
 249 
 250                 fm = get().getFocusModel();
 251                 if (fm != null) {
 252                     // similar to above, but this time for focus
 253                     fm.focusedIndexProperty().addListener(weakFocusedListener);
 254                 }
 255 
 256                 get().editingCellProperty().addListener(weakEditingListener);
 257 
 258                 weakTreeTableViewRef = new WeakReference<TreeTableView<T>>(get());
 259             }
 260 
 261             updateItem();
 262             requestLayout();
 263         }
 264     };
 265 
 266     private void setTreeTableView(TreeTableView<T> value) { treeTableView.set(value); }
 267 
 268     /**
 269      * Returns the TreeTableView associated with this TreeTableCell.
 270      * @return the tree table view
 271      */
 272     public final TreeTableView<T> getTreeTableView() { return treeTableView.get(); }
 273 
 274     /**
 275      * A TreeTableCell is explicitly linked to a single {@link TreeTableView} instance,
 276      * which is represented by this property.
 277      * @return the tree table view property
 278      */
 279     public final ReadOnlyObjectProperty<TreeTableView<T>> treeTableViewProperty() { return treeTableView.getReadOnlyProperty(); }
 280 
 281 
 282 
 283 
 284     /***************************************************************************
 285      *                                                                         *
 286      * Public API                                                              *
 287      *                                                                         *
 288      *************************************************************************
 289      * @param oldIndex
 290      * @param newIndex*/
 291 
 292 
 293     @Override void indexChanged(int oldIndex, int newIndex) {
 294         index = getIndex();
 295 
 296         // when the cell index changes, this may result in the cell
 297         // changing state to be selected and/or focused.
 298         updateItem();
 299         updateSelection();
 300         updateFocus();
 301 //        oldIndex = index;
 302     }
 303 
 304 
 305     /** {@inheritDoc} */
 306     @Override public void startEdit() {
 307         final TreeTableView<T> treeTable = getTreeTableView();
 308         if (! isEditable() || (treeTable != null && ! treeTable.isEditable())) {
 309             return;
 310         }
 311 
 312         // it makes sense to get the cell into its editing state before firing
 313         // the event to the TreeView below, so that's what we're doing here
 314         // by calling super.startEdit().
 315         super.startEdit();
 316 
 317          // Inform the TreeView of the edit starting.
 318         if (treeTable != null) {
 319             treeTable.fireEvent(new TreeTableView.EditEvent<T>(treeTable,
 320                     TreeTableView.<T>editStartEvent(),
 321                     getTreeItem(),
 322                     getItem(),
 323                     null));
 324 
 325             treeTable.requestFocus();
 326         }
 327     }
 328 
 329      /** {@inheritDoc} */
 330     @Override public void commitEdit(T newValue) {
 331         if (! isEditing()) return;
 332         final TreeItem<T> treeItem = getTreeItem();
 333         final TreeTableView<T> treeTable = getTreeTableView();
 334         if (treeTable != null) {
 335             // Inform the TreeView of the edit being ready to be committed.
 336             treeTable.fireEvent(new TreeTableView.EditEvent<T>(treeTable,
 337                     TreeTableView.<T>editCommitEvent(),
 338                     treeItem,
 339                     getItem(),
 340                     newValue));
 341         }
 342 
 343         // update the item within this cell, so that it represents the new value
 344         if (treeItem != null) {
 345             treeItem.setValue(newValue);
 346             updateTreeItem(treeItem);
 347             updateItem(newValue, false);
 348         }
 349 
 350         // inform parent classes of the commit, so that they can switch us
 351         // out of the editing state
 352         super.commitEdit(newValue);
 353 
 354         if (treeTable != null) {
 355             // reset the editing item in the TreetView
 356             treeTable.edit(-1, null);
 357             treeTable.requestFocus();
 358         }
 359     }
 360 
 361     /** {@inheritDoc} */
 362     @Override public void cancelEdit() {
 363         if (! isEditing()) return;
 364 
 365         TreeTableView<T> treeTable = getTreeTableView();
 366         if (treeTable != null) {
 367             treeTable.fireEvent(new TreeTableView.EditEvent<T>(treeTable,
 368                     TreeTableView.<T>editCancelEvent(),
 369                     getTreeItem(),
 370                     getItem(),
 371                     null));
 372         }
 373 
 374         super.cancelEdit();
 375 
 376         if (treeTable != null) {
 377             // reset the editing index on the TreeView
 378             treeTable.edit(-1, null);
 379             treeTable.requestFocus();
 380         }
 381     }
 382 
 383 
 384 
 385     /***************************************************************************
 386      *                                                                         *
 387      * Private Implementation                                                  *
 388      *                                                                         *
 389      **************************************************************************/
 390 
 391     private int index = -1;
 392     private boolean isFirstRun = true;
 393 
 394     private void updateItem() {
 395         TreeTableView<T> tv = getTreeTableView();
 396         if (tv == null) return;
 397 
 398         // Compute whether the index for this cell is for a real item
 399         boolean valid = index >=0 && index < tv.getExpandedItemCount();
 400 
 401         final TreeItem<T> oldTreeItem = getTreeItem();
 402         final boolean isEmpty = isEmpty();
 403 
 404         // Cause the cell to update itself
 405         if (valid) {
 406             // update the TreeCell state.
 407             // get the new treeItem that is about to go in to the TreeCell
 408             final TreeItem<T> newTreeItem = tv.getTreeItem(index);
 409             final T newValue = newTreeItem == null ? null : newTreeItem.getValue();
 410 
 411             // For the sake of RT-14279, it is important that the order of these
 412             // method calls is as shown below. If the order is switched, it is
 413             // likely that events will be fired where the item is null, even
 414             // though calling cell.getTreeItem().getValue() returns the value
 415             // as expected
 416 
 417             // There used to be conditional code here to prevent updateItem from
 418             // being called when the value didn't change, but that led us to
 419             // issues such as RT-33108, where the value didn't change but the item
 420             // we needed to be listening to did. Without calling updateItem we
 421             // were breaking things, so once again the conditionals are gone.
 422             updateTreeItem(newTreeItem);
 423             updateItem(newValue, false);
 424         } else {
 425             // RT-30484 We need to allow a first run to be special-cased to allow
 426             // for the updateItem method to be called at least once to allow for
 427             // the correct visual state to be set up. In particular, in RT-30484
 428             // refer to Ensemble8PopUpTree.png - in this case the arrows are being
 429             // shown as the new cells are instantiated with the arrows in the
 430             // children list, and are only hidden in updateItem.
 431             if ((!isEmpty && oldTreeItem != null) || isFirstRun) {
 432                 updateTreeItem(null);
 433                 updateItem(null, true);
 434                 isFirstRun = false;
 435             }
 436         }
 437     }
 438 
 439     private void updateSelection() {
 440         if (isEmpty()) return;
 441         if (index == -1 || getTreeTableView() == null) return;
 442         if (getTreeTableView().getSelectionModel() == null) return;
 443 
 444         boolean isSelected = getTreeTableView().getSelectionModel().isSelected(index);
 445         if (isSelected() == isSelected) return;
 446 
 447         updateSelected(isSelected);
 448     }
 449 
 450     private void updateFocus() {
 451         if (getIndex() == -1 || getTreeTableView() == null) return;
 452         if (getTreeTableView().getFocusModel() == null) return;
 453 
 454         setFocused(getTreeTableView().getFocusModel().isFocused(getIndex()));
 455     }
 456 
 457     private void updateEditing() {
 458         if (getIndex() == -1 || getTreeTableView() == null || getTreeItem() == null) return;
 459 
 460         final TreeTablePosition<T,?> editingCell = getTreeTableView().getEditingCell();
 461         if (editingCell != null && editingCell.getTableColumn() != null) {
 462             return;
 463         }
 464 
 465         final TreeItem<T> editItem = editingCell == null ? null : editingCell.getTreeItem();
 466         if (! isEditing() && getTreeItem().equals(editItem)) {
 467             startEdit();
 468         } else if (isEditing() && ! getTreeItem().equals(editItem)) {
 469             cancelEdit();
 470         }
 471     }
 472 
 473 
 474 
 475     /***************************************************************************
 476      *                                                                         *
 477      * Expert API                                                              *
 478      *                                                                         *
 479      **************************************************************************/
 480 
 481     /**
 482      * Updates the TreeTableView associated with this TreeTableCell.
 483      *
 484      * @param treeTable The new TreeTableView that should be associated with this
 485      *         TreeTableCell.
 486      * Note: This function is intended to be used by experts, primarily
 487      *         by those implementing new Skins. It is not common
 488      *         for developers or designers to access this function directly.
 489      */
 490     public final void updateTreeTableView(TreeTableView<T> treeTable) {
 491         setTreeTableView(treeTable);
 492     }
 493 
 494     /**
 495      * Updates the TreeItem associated with this TreeTableCell.
 496      *
 497      * @param treeItem The new TreeItem that should be associated with this
 498      *      TreeTableCell.
 499      * Note: This function is intended to be used by experts, primarily
 500      *       by those implementing new Skins. It is not common
 501      *       for developers or designers to access this function directly.
 502      */
 503     public final void updateTreeItem(TreeItem<T> treeItem) {
 504         TreeItem<T> _treeItem = getTreeItem();
 505         if (_treeItem != null) {
 506             _treeItem.leafProperty().removeListener(weakLeafListener);
 507         }
 508         setTreeItem(treeItem);
 509         if (treeItem != null) {
 510             treeItem.leafProperty().addListener(weakLeafListener);
 511         }
 512     }
 513 
 514 
 515 
 516     /***************************************************************************
 517      *                                                                         *
 518      * Stylesheet Handling                                                     *
 519      *                                                                         *
 520      **************************************************************************/
 521 
 522     private static final String DEFAULT_STYLE_CLASS = "tree-table-row-cell";
 523 
 524     private static final PseudoClass EXPANDED_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("expanded");
 525     private static final PseudoClass COLLAPSED_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("collapsed");
 526 
 527     /** {@inheritDoc} */
 528     @Override protected Skin<?> createDefaultSkin() {
 529         return new TreeTableRowSkin<T>(this);
 530     }
 531 
 532 
 533     /***************************************************************************
 534      *                                                                         *
 535      * Accessibility handling                                                  *
 536      *                                                                         *
 537      **************************************************************************/
 538 
 539     @Override
 540     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 541         final TreeItem<T> treeItem = getTreeItem();
 542         final TreeTableView<T> treeTableView = getTreeTableView();
 543 
 544         switch (attribute) {
 545             case TREE_ITEM_PARENT: {
 546                 if (treeItem == null) return null;
 547                 TreeItem<T> parent = treeItem.getParent();
 548                 if (parent == null) return null;
 549                 int parentIndex = treeTableView.getRow(parent);
 550                 return treeTableView.queryAccessibleAttribute(AccessibleAttribute.ROW_AT_INDEX, parentIndex);
 551             }
 552             case TREE_ITEM_COUNT: {
 553                 if (treeItem == null) return 0;
 554                 if (!treeItem.isExpanded()) return 0;
 555                 return treeItem.getChildren().size();
 556             }
 557             case TREE_ITEM_AT_INDEX: {
 558                 if (treeItem == null) return null;
 559                 if (!treeItem.isExpanded()) return null;
 560                 int index = (Integer)parameters[0];
 561                 if (index >= treeItem.getChildren().size()) return null;
 562                 TreeItem<T> child = treeItem.getChildren().get(index);
 563                 if (child == null) return null;
 564                 int childIndex = treeTableView.getRow(child);
 565                 return treeTableView.queryAccessibleAttribute(AccessibleAttribute.ROW_AT_INDEX, childIndex);
 566             }
 567             case LEAF: return treeItem == null ? true : treeItem.isLeaf();
 568             case EXPANDED: return treeItem == null ? false : treeItem.isExpanded();
 569             case INDEX: return getIndex();
 570             case DISCLOSURE_LEVEL: {
 571                 return treeTableView == null ? 0 : treeTableView.getTreeItemLevel(treeItem);
 572             }
 573             default: return super.queryAccessibleAttribute(attribute, parameters);
 574         }
 575     }
 576 
 577     @Override
 578     public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
 579         switch (action) {
 580             case EXPAND: {
 581                 TreeItem<T> treeItem = getTreeItem();
 582                 if (treeItem != null) treeItem.setExpanded(true);
 583                 break;
 584             }
 585             case COLLAPSE: {
 586                 TreeItem<T> treeItem = getTreeItem();
 587                 if (treeItem != null) treeItem.setExpanded(false);
 588                 break;
 589             }
 590             default: super.executeAccessibleAction(action);
 591         }
 592     }
 593 }