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 com.sun.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 }