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 }