1 /*
   2  * Copyright (c) 2012, 2014, 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      */
 175     public final TreeItem<T> getTreeItem() { return treeItem.get(); }
 176     
 177     /**
 178      * Each TreeTableCell represents at most a single {@link TreeItem}, which is
 179      * represented by this property.
 180      */
 181     public final ReadOnlyObjectProperty<TreeItem<T>> treeItemProperty() { return treeItem.getReadOnlyProperty(); }
 182 
 183     
 184     
 185     // --- Disclosure Node
 186     private ObjectProperty<Node> disclosureNode = new SimpleObjectProperty<Node>(this, "disclosureNode");
 187 
 188     /**
 189      * The node to use as the "disclosure" triangle, or toggle, used for
 190      * expanding and collapsing items. This is only used in the case of
 191      * an item in the tree which contains child items. If not specified, the
 192      * TreeTableCell's Skin implementation is responsible for providing a default
 193      * disclosure node.
 194      */
 195     public final void setDisclosureNode(Node value) { disclosureNodeProperty().set(value); }
 196     
 197     /**
 198      * Returns the current disclosure node set in this TreeTableCell.
 199      */
 200     public final Node getDisclosureNode() { return disclosureNode.get(); }
 201     
 202     /**
 203      * The disclosure node is commonly seen represented as a triangle that rotates
 204      * on screen to indicate whether or not the TreeItem that it is placed
 205      * beside is expanded or collapsed.
 206      */
 207     public final ObjectProperty<Node> disclosureNodeProperty() { return disclosureNode; }
 208     
 209     
 210     // --- TreeView
 211     private ReadOnlyObjectWrapper<TreeTableView<T>> treeTableView = new ReadOnlyObjectWrapper<TreeTableView<T>>(this, "treeTableView") {
 212         private WeakReference<TreeTableView<T>> weakTreeTableViewRef;
 213         @Override protected void invalidated() {
 214             TreeTableViewSelectionModel<T> sm;
 215             TreeTableViewFocusModel<T> fm;
 216             
 217             if (weakTreeTableViewRef != null) {
 218                 TreeTableView<T> oldTreeTableView = weakTreeTableViewRef.get();
 219                 if (oldTreeTableView != null) {
 220                     // remove old listeners
 221                     sm = oldTreeTableView.getSelectionModel();
 222                     if (sm != null) {
 223                         sm.getSelectedIndices().removeListener(weakSelectedListener);
 224                     }
 225 
 226                     fm = oldTreeTableView.getFocusModel();
 227                     if (fm != null) {
 228                         fm.focusedIndexProperty().removeListener(weakFocusedListener);
 229                     }
 230 
 231                     oldTreeTableView.editingCellProperty().removeListener(weakEditingListener);
 232                 }
 233                 
 234                 weakTreeTableViewRef = null;
 235             }
 236 
 237             if (get() != null) {
 238                 sm = get().getSelectionModel();
 239                 if (sm != null) {
 240                     // listening for changes to treeView.selectedIndex and IndexedCell.index,
 241                     // to determine if this cell is selected
 242                     sm.getSelectedIndices().addListener(weakSelectedListener);
 243                 }
 244 
 245                 fm = get().getFocusModel();
 246                 if (fm != null) {
 247                     // similar to above, but this time for focus
 248                     fm.focusedIndexProperty().addListener(weakFocusedListener);
 249                 }
 250 
 251                 get().editingCellProperty().addListener(weakEditingListener);
 252                 
 253                 weakTreeTableViewRef = new WeakReference<TreeTableView<T>>(get());
 254             }
 255 
 256             updateItem();
 257             requestLayout();
 258         }
 259     };
 260     
 261     private void setTreeTableView(TreeTableView<T> value) { treeTableView.set(value); }
 262 
 263     /**
 264      * Returns the TreeTableView associated with this TreeTableCell.
 265      */
 266     public final TreeTableView<T> getTreeTableView() { return treeTableView.get(); }
 267     
 268     /**
 269      * A TreeTableCell is explicitly linked to a single {@link TreeTableView} instance,
 270      * which is represented by this property.
 271      */
 272     public final ReadOnlyObjectProperty<TreeTableView<T>> treeTableViewProperty() { return treeTableView.getReadOnlyProperty(); }
 273 
 274 
 275 
 276 
 277     /***************************************************************************
 278      *                                                                         *
 279      * Public API                                                              *
 280      *                                                                         *
 281      *************************************************************************
 282      * @param oldIndex
 283      * @param newIndex*/
 284 
 285     
 286     @Override void indexChanged(int oldIndex, int newIndex) {
 287         index = getIndex();
 288 
 289         // when the cell index changes, this may result in the cell
 290         // changing state to be selected and/or focused.
 291         updateItem();
 292         updateSelection();
 293         updateFocus();
 294 //        oldIndex = index;
 295     }
 296     
 297 
 298     /** {@inheritDoc} */
 299     @Override public void startEdit() {
 300         final TreeTableView<T> treeTable = getTreeTableView();
 301         if (! isEditable() || (treeTable != null && ! treeTable.isEditable())) {
 302             return;
 303         }
 304         
 305         // it makes sense to get the cell into its editing state before firing
 306         // the event to the TreeView below, so that's what we're doing here
 307         // by calling super.startEdit().
 308         super.startEdit();
 309         
 310          // Inform the TreeView of the edit starting.
 311         if (treeTable != null) {
 312             treeTable.fireEvent(new TreeTableView.EditEvent<T>(treeTable,
 313                     TreeTableView.<T>editStartEvent(),
 314                     getTreeItem(),
 315                     getItem(),
 316                     null));
 317             
 318             treeTable.requestFocus();
 319         }
 320     }
 321 
 322      /** {@inheritDoc} */
 323     @Override public void commitEdit(T newValue) {
 324         if (! isEditing()) return;
 325         final TreeItem<T> treeItem = getTreeItem();
 326         final TreeTableView<T> treeTable = getTreeTableView();
 327         if (treeTable != null) {
 328             // Inform the TreeView of the edit being ready to be committed.
 329             treeTable.fireEvent(new TreeTableView.EditEvent<T>(treeTable,
 330                     TreeTableView.<T>editCommitEvent(),
 331                     treeItem,
 332                     getItem(),
 333                     newValue));
 334         }
 335         
 336         // update the item within this cell, so that it represents the new value
 337         if (treeItem != null) {
 338             treeItem.setValue(newValue);
 339             updateTreeItem(treeItem);
 340             updateItem(newValue, false);
 341         }
 342         
 343         // inform parent classes of the commit, so that they can switch us
 344         // out of the editing state
 345         super.commitEdit(newValue);
 346 
 347         if (treeTable != null) {
 348             // reset the editing item in the TreetView
 349             treeTable.edit(-1, null);
 350             treeTable.requestFocus();
 351         }
 352     }
 353 
 354     /** {@inheritDoc} */
 355     @Override public void cancelEdit() {
 356         if (! isEditing()) return;
 357         
 358         TreeTableView<T> treeTable = getTreeTableView();
 359         if (treeTable != null) {
 360             treeTable.fireEvent(new TreeTableView.EditEvent<T>(treeTable,
 361                     TreeTableView.<T>editCancelEvent(),
 362                     getTreeItem(),
 363                     getItem(),
 364                     null));
 365         }
 366 
 367         super.cancelEdit();
 368 
 369         if (treeTable != null) {
 370             // reset the editing index on the TreeView
 371             treeTable.edit(-1, null);
 372             treeTable.requestFocus();
 373         }
 374     }
 375 
 376 
 377 
 378     /***************************************************************************
 379      *                                                                         *
 380      * Private Implementation                                                  *
 381      *                                                                         *
 382      **************************************************************************/
 383     
 384     private int index = -1;
 385     private boolean isFirstRun = true;
 386 
 387     private void updateItem() {
 388         TreeTableView<T> tv = getTreeTableView();
 389         if (tv == null) return;
 390         
 391         // Compute whether the index for this cell is for a real item
 392         boolean valid = index >=0 && index < tv.getExpandedItemCount();
 393 
 394         final TreeItem<T> oldTreeItem = getTreeItem();
 395         final boolean isEmpty = isEmpty();
 396 
 397         // Cause the cell to update itself
 398         if (valid) {
 399             // update the TreeCell state.
 400             // get the new treeItem that is about to go in to the TreeCell
 401             final TreeItem<T> newTreeItem = tv.getTreeItem(index);
 402             final T newValue = newTreeItem == null ? null : newTreeItem.getValue();
 403             
 404             // For the sake of RT-14279, it is important that the order of these
 405             // method calls is as shown below. If the order is switched, it is
 406             // likely that events will be fired where the item is null, even
 407             // though calling cell.getTreeItem().getValue() returns the value
 408             // as expected
 409 
 410             // There used to be conditional code here to prevent updateItem from
 411             // being called when the value didn't change, but that led us to
 412             // issues such as RT-33108, where the value didn't change but the item
 413             // we needed to be listening to did. Without calling updateItem we
 414             // were breaking things, so once again the conditionals are gone.
 415             updateTreeItem(newTreeItem);
 416             updateItem(newValue, false);
 417         } else {
 418             // RT-30484 We need to allow a first run to be special-cased to allow
 419             // for the updateItem method to be called at least once to allow for
 420             // the correct visual state to be set up. In particular, in RT-30484
 421             // refer to Ensemble8PopUpTree.png - in this case the arrows are being
 422             // shown as the new cells are instantiated with the arrows in the
 423             // children list, and are only hidden in updateItem.
 424             if ((!isEmpty && oldTreeItem != null) || isFirstRun) {
 425                 updateTreeItem(null);
 426                 updateItem(null, true);
 427                 isFirstRun = false;
 428             }
 429         }
 430     }
 431 
 432     private void updateSelection() {
 433         if (isEmpty()) return;
 434         if (index == -1 || getTreeTableView() == null) return;
 435         if (getTreeTableView().getSelectionModel() == null) return;
 436         
 437         boolean isSelected = getTreeTableView().getSelectionModel().isSelected(index);
 438         if (isSelected() == isSelected) return;
 439         
 440         updateSelected(isSelected);
 441     }
 442 
 443     private void updateFocus() {
 444         if (getIndex() == -1 || getTreeTableView() == null) return;
 445         if (getTreeTableView().getFocusModel() == null) return;
 446         
 447         setFocused(getTreeTableView().getFocusModel().isFocused(getIndex()));
 448     }
 449 
 450     private void updateEditing() {
 451         if (getIndex() == -1 || getTreeTableView() == null || getTreeItem() == null) return;
 452 
 453         final TreeTablePosition<T,?> editingCell = getTreeTableView().getEditingCell();
 454         if (editingCell != null && editingCell.getTableColumn() != null) {
 455             return;
 456         }
 457 
 458         final TreeItem<T> editItem = editingCell == null ? null : editingCell.getTreeItem();
 459         if (! isEditing() && getTreeItem().equals(editItem)) {
 460             startEdit();
 461         } else if (isEditing() && ! getTreeItem().equals(editItem)) {
 462             cancelEdit();
 463         }
 464     }
 465 
 466 
 467 
 468     /***************************************************************************
 469      *                                                                         *
 470      * Expert API                                                              *
 471      *                                                                         *
 472      **************************************************************************/
 473 
 474     /**
 475      * Updates the TreeTableView associated with this TreeTableCell.
 476      * 
 477      * @param treeTable The new TreeTableView that should be associated with this
 478      *         TreeTableCell.
 479      * @expert This function is intended to be used by experts, primarily
 480      *         by those implementing new Skins. It is not common
 481      *         for developers or designers to access this function directly.
 482      */
 483     public final void updateTreeTableView(TreeTableView<T> treeTable) {
 484         setTreeTableView(treeTable); 
 485     }
 486 
 487     /**
 488      * Updates the TreeItem associated with this TreeTableCell.
 489      *
 490      * @param treeItem The new TreeItem that should be associated with this 
 491      *      TreeTableCell.
 492      * @expert This function is intended to be used by experts, primarily
 493      *      by those implementing new Skins. It is not common
 494      *      for developers or designers to access this function directly.
 495      */
 496     public final void updateTreeItem(TreeItem<T> treeItem) {
 497         TreeItem<T> _treeItem = getTreeItem();
 498         if (_treeItem != null) {
 499             _treeItem.leafProperty().removeListener(weakLeafListener);
 500         }
 501         setTreeItem(treeItem);
 502         if (treeItem != null) {
 503             treeItem.leafProperty().addListener(weakLeafListener);
 504         }
 505     }
 506 
 507 
 508     
 509     /***************************************************************************
 510      *                                                                         *
 511      * Stylesheet Handling                                                     *
 512      *                                                                         *
 513      **************************************************************************/
 514 
 515     private static final String DEFAULT_STYLE_CLASS = "tree-table-row-cell";
 516 
 517     private static final PseudoClass EXPANDED_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("expanded");
 518     private static final PseudoClass COLLAPSED_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("collapsed");
 519     
 520     /** {@inheritDoc} */
 521     @Override protected Skin<?> createDefaultSkin() {
 522         return new TreeTableRowSkin<T>(this);
 523     }
 524 
 525 
 526     /***************************************************************************
 527      *                                                                         *
 528      * Accessibility handling                                                  *
 529      *                                                                         *
 530      **************************************************************************/
 531 
 532     @Override
 533     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 534         final TreeItem<T> treeItem = getTreeItem();
 535         final TreeTableView<T> treeTableView = getTreeTableView();
 536 
 537         switch (attribute) {
 538             case TREE_ITEM_PARENT: {
 539                 if (treeItem == null) return null;
 540                 TreeItem<T> parent = treeItem.getParent();
 541                 if (parent == null) return null;
 542                 int parentIndex = treeTableView.getRow(parent);
 543                 return treeTableView.queryAccessibleAttribute(AccessibleAttribute.ROW_AT_INDEX, parentIndex);
 544             }
 545             case TREE_ITEM_COUNT: {
 546                 if (treeItem == null) return 0;
 547                 if (!treeItem.isExpanded()) return 0;
 548                 return treeItem.getChildren().size();
 549             }
 550             case TREE_ITEM_AT_INDEX: {
 551                 if (treeItem == null) return null;
 552                 if (!treeItem.isExpanded()) return null;
 553                 int index = (Integer)parameters[0];
 554                 if (index >= treeItem.getChildren().size()) return null;
 555                 TreeItem<T> child = treeItem.getChildren().get(index);
 556                 if (child == null) return null;
 557                 int childIndex = treeTableView.getRow(child);
 558                 return treeTableView.queryAccessibleAttribute(AccessibleAttribute.ROW_AT_INDEX, childIndex);
 559             }
 560             case LEAF: return treeItem == null ? true : treeItem.isLeaf();
 561             case EXPANDED: return treeItem == null ? false : treeItem.isExpanded();
 562             case INDEX: return getIndex();
 563             case DISCLOSURE_LEVEL: {
 564                 return treeTableView == null ? 0 : treeTableView.getTreeItemLevel(treeItem);
 565             }
 566             default: return super.queryAccessibleAttribute(attribute, parameters);
 567         }
 568     }
 569 
 570     @Override
 571     public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
 572         switch (action) {
 573             case EXPAND: {
 574                 TreeItem<T> treeItem = getTreeItem();
 575                 if (treeItem != null) treeItem.setExpanded(true);
 576                 break;
 577             }
 578             case COLLAPSE: {
 579                 TreeItem<T> treeItem = getTreeItem();
 580                 if (treeItem != null) treeItem.setExpanded(false);
 581                 break;
 582             }
 583             default: super.executeAccessibleAction(action);
 584         }
 585     }
 586 }