1 /* 2 * Copyright (c) 2008, 2016, 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.converter.SizeConverter; 29 import com.sun.javafx.scene.control.Properties; 30 import com.sun.javafx.scene.control.behavior.TreeCellBehavior; 31 import javafx.scene.control.skin.TreeViewSkin; 32 33 import javafx.application.Platform; 34 import javafx.beans.DefaultProperty; 35 import javafx.beans.property.BooleanProperty; 36 import javafx.beans.property.DoubleProperty; 37 import javafx.beans.property.ObjectProperty; 38 import javafx.beans.property.ObjectPropertyBase; 39 import javafx.beans.property.ReadOnlyIntegerProperty; 40 import javafx.beans.property.ReadOnlyIntegerWrapper; 41 import javafx.beans.property.ReadOnlyObjectProperty; 42 import javafx.beans.property.ReadOnlyObjectWrapper; 43 import javafx.beans.property.SimpleBooleanProperty; 44 import javafx.beans.property.SimpleObjectProperty; 45 import javafx.beans.value.ChangeListener; 46 import javafx.beans.value.WeakChangeListener; 47 import javafx.beans.value.WritableValue; 48 import javafx.collections.ListChangeListener; 49 import javafx.css.CssMetaData; 50 import javafx.css.Styleable; 51 import javafx.css.StyleableDoubleProperty; 52 import javafx.css.StyleableProperty; 53 import javafx.event.Event; 54 import javafx.event.EventHandler; 55 import javafx.event.EventType; 56 import javafx.event.WeakEventHandler; 57 import javafx.scene.AccessibleAttribute; 58 import javafx.scene.AccessibleRole; 59 import javafx.scene.control.TreeItem.TreeModificationEvent; 60 import javafx.scene.layout.Region; 61 import javafx.util.Callback; 62 63 import java.lang.ref.SoftReference; 64 import java.lang.ref.WeakReference; 65 import java.util.ArrayList; 66 import java.util.Collections; 67 import java.util.HashMap; 68 import java.util.List; 69 import java.util.Map; 70 71 /** 72 * The TreeView control provides a view on to a tree root (of type 73 * {@link TreeItem}). By using a TreeView, it is possible to drill down into the 74 * children of a TreeItem, recursively until a TreeItem has no children (that is, 75 * it is a <i>leaf</i> node in the tree). To facilitate this, unlike controls 76 * like {@link ListView}, in TreeView it is necessary to <strong>only</strong> 77 * specify the {@link #rootProperty() root} node. 78 * 79 * <p> 80 * For more information on building up a tree using this approach, refer to the 81 * {@link TreeItem} class documentation. Briefly however, to create a TreeView, 82 * you should do something along the lines of the following: 83 * <pre><code> 84 * TreeItem<String> root = new TreeItem<String>("Root Node"); 85 * root.setExpanded(true); 86 * root.getChildren().addAll( 87 * new TreeItem<String>("Item 1"), 88 * new TreeItem<String>("Item 2"), 89 * new TreeItem<String>("Item 3") 90 * ); 91 * TreeView<String> treeView = new TreeView<String>(root); 92 * </code></pre> 93 * 94 * <p> 95 * A TreeView may be configured to optionally hide the root node by setting the 96 * {@link #setShowRoot(boolean) showRoot} property to {@code false}. If the root 97 * node is hidden, there is one less level of indentation, and all children 98 * nodes of the root node are shown. By default, the root node is shown in the 99 * TreeView. 100 * 101 * <h3>TreeView Selection / Focus APIs</h3> 102 * <p>To track selection and focus, it is necessary to become familiar with the 103 * {@link SelectionModel} and {@link FocusModel} classes. A TreeView has at most 104 * one instance of each of these classes, available from 105 * {@link #selectionModelProperty() selectionModel} and 106 * {@link #focusModelProperty() focusModel} properties respectively. 107 * Whilst it is possible to use this API to set a new selection model, in 108 * most circumstances this is not necessary - the default selection and focus 109 * models should work in most circumstances. 110 * 111 * <p>The default {@link SelectionModel} used when instantiating a TreeView is 112 * an implementation of the {@link MultipleSelectionModel} abstract class. 113 * However, as noted in the API documentation for 114 * the {@link MultipleSelectionModel#selectionModeProperty() selectionMode} 115 * property, the default value is {@link SelectionMode#SINGLE}. To enable 116 * multiple selection in a default TreeView instance, it is therefore necessary 117 * to do the following: 118 * 119 * <pre> 120 * {@code 121 * treeView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);}</pre> 122 * 123 * <h3>Customizing TreeView Visuals</h3> 124 * <p>The visuals of the TreeView can be entirely customized by replacing the 125 * default {@link #cellFactoryProperty() cell factory}. A cell factory is used to 126 * generate {@link TreeCell} instances, which are used to represent an item in the 127 * TreeView. See the {@link Cell} class documentation for a more complete 128 * description of how to write custom Cells. 129 * 130 * <h3>Editing</h3> 131 * <p>This control supports inline editing of values, and this section attempts to 132 * give an overview of the available APIs and how you should use them.</p> 133 * 134 * <p>Firstly, cell editing most commonly requires a different user interface 135 * than when a cell is not being edited. This is the responsibility of the 136 * {@link Cell} implementation being used. For TreeView, this is the responsibility 137 * of the {@link #cellFactoryProperty() cell factory}. It is your choice whether the cell is 138 * permanently in an editing state (e.g. this is common for {@link CheckBox} cells), 139 * or to switch to a different UI when editing begins (e.g. when a double-click 140 * is received on a cell).</p> 141 * 142 * <p>To know when editing has been requested on a cell, 143 * simply override the {@link javafx.scene.control.Cell#startEdit()} method, and 144 * update the cell {@link javafx.scene.control.Cell#textProperty() text} and 145 * {@link javafx.scene.control.Cell#graphicProperty() graphic} properties as 146 * appropriate (e.g. set the text to null and set the graphic to be a 147 * {@link TextField}). Additionally, you should also override 148 * {@link Cell#cancelEdit()} to reset the UI back to its original visual state 149 * when the editing concludes. In both cases it is important that you also 150 * ensure that you call the super method to have the cell perform all duties it 151 * must do to enter or exit its editing mode.</p> 152 * 153 * <p>Once your cell is in an editing state, the next thing you are most probably 154 * interested in is how to commit or cancel the editing that is taking place. This is your 155 * responsibility as the cell factory provider. Your cell implementation will know 156 * when the editing is over, based on the user input (e.g. when the user presses 157 * the Enter or ESC keys on their keyboard). When this happens, it is your 158 * responsibility to call {@link Cell#commitEdit(Object)} or 159 * {@link Cell#cancelEdit()}, as appropriate.</p> 160 * 161 * <p>When you call {@link Cell#commitEdit(Object)} an event is fired to the 162 * TreeView, which you can observe by adding an {@link EventHandler} via 163 * {@link TreeView#setOnEditCommit(javafx.event.EventHandler)}. Similarly, 164 * you can also observe edit events for 165 * {@link TreeView#setOnEditStart(javafx.event.EventHandler) edit start} 166 * and {@link TreeView#setOnEditCancel(javafx.event.EventHandler) edit cancel}.</p> 167 * 168 * <p>By default the TreeView edit commit handler is non-null, with a default 169 * handler that attempts to overwrite the property value for the 170 * item in the currently-being-edited row. It is able to do this as the 171 * {@link Cell#commitEdit(Object)} method is passed in the new value, and this 172 * is passed along to the edit commit handler via the 173 * {@link EditEvent} that is fired. It is simply a matter of calling 174 * {@link EditEvent#getNewValue()} to retrieve this value. 175 * 176 * <p>It is very important to note that if you call 177 * {@link TreeView#setOnEditCommit(javafx.event.EventHandler)} with your own 178 * {@link EventHandler}, then you will be removing the default handler. Unless 179 * you then handle the writeback to the property (or the relevant data source), 180 * nothing will happen. You can work around this by using the 181 * {@link TreeView#addEventHandler(javafx.event.EventType, javafx.event.EventHandler)} 182 * method to add a {@link TreeView#EDIT_COMMIT_EVENT} {@link EventType} with 183 * your desired {@link EventHandler} as the second argument. Using this method, 184 * you will not replace the default implementation, but you will be notified when 185 * an edit commit has occurred.</p> 186 * 187 * <p>Hopefully this summary answers some of the commonly asked questions. 188 * Fortunately, JavaFX ships with a number of pre-built cell factories that 189 * handle all the editing requirements on your behalf. You can find these 190 * pre-built cell factories in the javafx.scene.control.cell package.</p> 191 * 192 * @see TreeItem 193 * @see TreeCell 194 * @param <T> The type of the item contained within the {@link TreeItem} value 195 * property for all tree items in this TreeView. 196 * @since JavaFX 2.0 197 */ 198 @DefaultProperty("root") 199 public class TreeView<T> extends Control { 200 201 /*************************************************************************** 202 * * 203 * Static properties and methods * 204 * * 205 **************************************************************************/ 206 207 /** 208 * An EventType that indicates some edit event has occurred. It is the parent 209 * type of all other edit events: {@link #editStartEvent}, 210 * {@link #editCommitEvent} and {@link #editCancelEvent}. 211 * 212 * @return An EventType that indicates some edit event has occurred. 213 */ 214 @SuppressWarnings("unchecked") 215 public static <T> EventType<EditEvent<T>> editAnyEvent() { 216 return (EventType<EditEvent<T>>) EDIT_ANY_EVENT; 217 } 218 private static final EventType<?> EDIT_ANY_EVENT = 219 new EventType<>(Event.ANY, "TREE_VIEW_EDIT"); 220 221 /** 222 * An EventType used to indicate that an edit event has started within the 223 * TreeView upon which the event was fired. 224 * 225 * @return An EventType used to indicate that an edit event has started. 226 */ 227 @SuppressWarnings("unchecked") 228 public static <T> EventType<EditEvent<T>> editStartEvent() { 229 return (EventType<EditEvent<T>>) EDIT_START_EVENT; 230 } 231 private static final EventType<?> EDIT_START_EVENT = 232 new EventType<>(editAnyEvent(), "EDIT_START"); 233 234 /** 235 * An EventType used to indicate that an edit event has just been canceled 236 * within the TreeView upon which the event was fired. 237 * 238 * @return An EventType used to indicate that an edit event has just been 239 * canceled. 240 */ 241 @SuppressWarnings("unchecked") 242 public static <T> EventType<EditEvent<T>> editCancelEvent() { 243 return (EventType<EditEvent<T>>) EDIT_CANCEL_EVENT; 244 } 245 private static final EventType<?> EDIT_CANCEL_EVENT = 246 new EventType<>(editAnyEvent(), "EDIT_CANCEL"); 247 248 /** 249 * An EventType that is used to indicate that an edit in a TreeView has been 250 * committed. This means that user has made changes to the data of a 251 * TreeItem, and that the UI should be updated. 252 * 253 * @return An EventType that is used to indicate that an edit in a TreeView 254 * has been committed. 255 */ 256 @SuppressWarnings("unchecked") 257 public static <T> EventType<EditEvent<T>> editCommitEvent() { 258 return (EventType<EditEvent<T>>) EDIT_COMMIT_EVENT; 259 } 260 private static final EventType<?> EDIT_COMMIT_EVENT = 261 new EventType<>(editAnyEvent(), "EDIT_COMMIT"); 262 263 /** 264 * Returns the number of levels of 'indentation' of the given TreeItem, 265 * based on how many times {@link javafx.scene.control.TreeItem#getParent()} 266 * can be recursively called. If the TreeItem does not have any parent set, 267 * the returned value will be zero. For each time getParent() is recursively 268 * called, the returned value is incremented by one. 269 * 270 * <p><strong>Important note: </strong>This method is deprecated as it does 271 * not consider the root node. This means that this method will iterate 272 * past the root node of the TreeView control, if the root node has a parent. 273 * If this is important, call {@link TreeView#getTreeItemLevel(TreeItem)} 274 * instead. 275 * 276 * @param node The TreeItem for which the level is needed. 277 * @return An integer representing the number of parents above the given node, 278 * or -1 if the given TreeItem is null. 279 * @deprecated This method does not correctly calculate the distance from the 280 * given TreeItem to the root of the TreeView. As of JavaFX 8.0_20, 281 * the proper way to do this is via 282 * {@link TreeView#getTreeItemLevel(TreeItem)} 283 */ 284 @Deprecated(since="8u20") 285 public static int getNodeLevel(TreeItem<?> node) { 286 if (node == null) return -1; 287 288 int level = 0; 289 TreeItem<?> parent = node.getParent(); 290 while (parent != null) { 291 level++; 292 parent = parent.getParent(); 293 } 294 295 return level; 296 } 297 298 299 /*************************************************************************** 300 * * 301 * Constructors * 302 * * 303 **************************************************************************/ 304 305 /** 306 * Creates an empty TreeView. 307 * 308 * <p>Refer to the {@link TreeView} class documentation for details on the 309 * default state of other properties. 310 */ 311 public TreeView() { 312 this(null); 313 } 314 315 /** 316 * Creates a TreeView with the provided root node. 317 * 318 * <p>Refer to the {@link TreeView} class documentation for details on the 319 * default state of other properties. 320 * 321 * @param root The node to be the root in this TreeView. 322 */ 323 public TreeView(TreeItem<T> root) { 324 getStyleClass().setAll(DEFAULT_STYLE_CLASS); 325 setAccessibleRole(AccessibleRole.TREE_VIEW); 326 327 setRoot(root); 328 updateExpandedItemCount(root); 329 330 // install default selection and focus models - it's unlikely this will be changed 331 // by many users. 332 MultipleSelectionModel<TreeItem<T>> sm = new TreeViewBitSetSelectionModel<T>(this); 333 setSelectionModel(sm); 334 setFocusModel(new TreeViewFocusModel<T>(this)); 335 } 336 337 338 339 /*************************************************************************** 340 * * 341 * Instance Variables * 342 * * 343 **************************************************************************/ 344 345 // used in the tree item modification event listener. Used by the 346 // layoutChildren method to determine whether the tree item count should 347 // be recalculated. 348 private boolean expandedItemCountDirty = true; 349 350 // Used in the getTreeItem(int row) method to act as a cache. 351 // See RT-26716 for the justification and performance gains. 352 private Map<Integer, SoftReference<TreeItem<T>>> treeItemCacheMap = new HashMap<>(); 353 354 355 /*************************************************************************** 356 * * 357 * Callbacks and Events * 358 * * 359 **************************************************************************/ 360 361 // we use this to forward events that have bubbled up TreeItem instances 362 // to the TreeViewSkin, to force it to recalculate teh item count and redraw 363 // if necessary 364 private final EventHandler<TreeModificationEvent<T>> rootEvent = e -> { 365 // this forces layoutChildren at the next pulse, and therefore 366 // updates the item count if necessary 367 EventType<?> eventType = e.getEventType(); 368 boolean match = false; 369 while (eventType != null) { 370 if (eventType.equals(TreeItem.<T>expandedItemCountChangeEvent())) { 371 match = true; 372 break; 373 } 374 eventType = eventType.getSuperType(); 375 } 376 377 if (match) { 378 expandedItemCountDirty = true; 379 requestLayout(); 380 } 381 }; 382 383 private WeakEventHandler<TreeModificationEvent<T>> weakRootEventListener; 384 385 386 387 /*************************************************************************** 388 * * 389 * Properties * 390 * * 391 **************************************************************************/ 392 393 394 // --- Cell Factory 395 private ObjectProperty<Callback<TreeView<T>, TreeCell<T>>> cellFactory; 396 397 /** 398 * Sets the cell factory that will be used for creating TreeCells, 399 * which are used to represent items in the 400 * TreeView. The factory works identically to the cellFactory in ListView 401 * and other complex composite controls. It is called to create a new 402 * TreeCell only when the system has determined that it doesn't have enough 403 * cells to represent the currently visible items. The TreeCell is reused 404 * by the system to represent different items in the tree when possible. 405 * 406 * <p>Refer to the {@link Cell} class documentation for more details. 407 * 408 * @param value The {@link Callback} to use for generating TreeCell instances, 409 * or null if the default cell factory should be used. 410 */ 411 public final void setCellFactory(Callback<TreeView<T>, TreeCell<T>> value) { 412 cellFactoryProperty().set(value); 413 } 414 415 /** 416 * <p>Returns the cell factory that will be used for creating TreeCells, 417 * which are used to represent items in the TreeView, or null if no custom 418 * cell factory has been set. 419 */ 420 public final Callback<TreeView<T>, TreeCell<T>> getCellFactory() { 421 return cellFactory == null ? null : cellFactory.get(); 422 } 423 424 /** 425 * Represents the cell factory that will be used for creating TreeCells, 426 * which are used to represent items in the TreeView. 427 */ 428 public final ObjectProperty<Callback<TreeView<T>, TreeCell<T>>> cellFactoryProperty() { 429 if (cellFactory == null) { 430 cellFactory = new SimpleObjectProperty<Callback<TreeView<T>, TreeCell<T>>>(this, "cellFactory"); 431 } 432 return cellFactory; 433 } 434 435 436 // --- Root 437 private ObjectProperty<TreeItem<T>> root = new SimpleObjectProperty<TreeItem<T>>(this, "root") { 438 private WeakReference<TreeItem<T>> weakOldItem; 439 440 @Override protected void invalidated() { 441 TreeItem<T> oldTreeItem = weakOldItem == null ? null : weakOldItem.get(); 442 if (oldTreeItem != null && weakRootEventListener != null) { 443 oldTreeItem.removeEventHandler(TreeItem.<T>treeNotificationEvent(), weakRootEventListener); 444 } 445 446 TreeItem<T> root = getRoot(); 447 if (root != null) { 448 weakRootEventListener = new WeakEventHandler<>(rootEvent); 449 getRoot().addEventHandler(TreeItem.<T>treeNotificationEvent(), weakRootEventListener); 450 weakOldItem = new WeakReference<>(root); 451 } 452 453 // Fix for RT-37853 454 edit(null); 455 456 expandedItemCountDirty = true; 457 updateRootExpanded(); 458 } 459 }; 460 461 /** 462 * Sets the root node in this TreeView. See the {@link TreeItem} class level 463 * documentation for more details. 464 * 465 * @param value The {@link TreeItem} that will be placed at the root of the 466 * TreeView. 467 */ 468 public final void setRoot(TreeItem<T> value) { 469 rootProperty().set(value); 470 } 471 472 /** 473 * Returns the current root node of this TreeView, or null if no root node 474 * is specified. 475 * @return The current root node, or null if no root node exists. 476 */ 477 public final TreeItem<T> getRoot() { 478 return root == null ? null : root.get(); 479 } 480 481 /** 482 * Property representing the root node of the TreeView. 483 */ 484 public final ObjectProperty<TreeItem<T>> rootProperty() { 485 return root; 486 } 487 488 489 490 // --- Show Root 491 private BooleanProperty showRoot; 492 493 /** 494 * Specifies whether the root {@code TreeItem} should be shown within this 495 * TreeView. 496 * 497 * @param value If true, the root TreeItem will be shown, and if false it 498 * will be hidden. 499 */ 500 public final void setShowRoot(boolean value) { 501 showRootProperty().set(value); 502 } 503 504 /** 505 * Returns true if the root of the TreeView should be shown, and false if 506 * it should not. By default, the root TreeItem is visible in the TreeView. 507 */ 508 public final boolean isShowRoot() { 509 return showRoot == null ? true : showRoot.get(); 510 } 511 512 /** 513 * Property that represents whether or not the TreeView root node is visible. 514 */ 515 public final BooleanProperty showRootProperty() { 516 if (showRoot == null) { 517 showRoot = new SimpleBooleanProperty(this, "showRoot", true) { 518 @Override protected void invalidated() { 519 updateRootExpanded(); 520 updateExpandedItemCount(getRoot()); 521 } 522 }; 523 } 524 return showRoot; 525 } 526 527 528 // --- Selection Model 529 private ObjectProperty<MultipleSelectionModel<TreeItem<T>>> selectionModel; 530 531 /** 532 * Sets the {@link MultipleSelectionModel} to be used in the TreeView. 533 * Despite a TreeView requiring a <code><b>Multiple</b>SelectionModel</code>, 534 * it is possible to configure it to only allow single selection (see 535 * {@link MultipleSelectionModel#setSelectionMode(javafx.scene.control.SelectionMode)} 536 * for more information). 537 */ 538 public final void setSelectionModel(MultipleSelectionModel<TreeItem<T>> value) { 539 selectionModelProperty().set(value); 540 } 541 542 /** 543 * Returns the currently installed selection model. 544 */ 545 public final MultipleSelectionModel<TreeItem<T>> getSelectionModel() { 546 return selectionModel == null ? null : selectionModel.get(); 547 } 548 549 /** 550 * The SelectionModel provides the API through which it is possible 551 * to select single or multiple items within a TreeView, as well as inspect 552 * which rows have been selected by the user. Note that it has a generic 553 * type that must match the type of the TreeView itself. 554 */ 555 public final ObjectProperty<MultipleSelectionModel<TreeItem<T>>> selectionModelProperty() { 556 if (selectionModel == null) { 557 selectionModel = new SimpleObjectProperty<MultipleSelectionModel<TreeItem<T>>>(this, "selectionModel"); 558 } 559 return selectionModel; 560 } 561 562 563 // --- Focus Model 564 private ObjectProperty<FocusModel<TreeItem<T>>> focusModel; 565 566 /** 567 * Sets the {@link FocusModel} to be used in the TreeView. 568 */ 569 public final void setFocusModel(FocusModel<TreeItem<T>> value) { 570 focusModelProperty().set(value); 571 } 572 573 /** 574 * Returns the currently installed {@link FocusModel}. 575 */ 576 public final FocusModel<TreeItem<T>> getFocusModel() { 577 return focusModel == null ? null : focusModel.get(); 578 } 579 580 /** 581 * The FocusModel provides the API through which it is possible 582 * to control focus on zero or one rows of the TreeView. Generally the 583 * default implementation should be more than sufficient. 584 */ 585 public final ObjectProperty<FocusModel<TreeItem<T>>> focusModelProperty() { 586 if (focusModel == null) { 587 focusModel = new SimpleObjectProperty<FocusModel<TreeItem<T>>>(this, "focusModel"); 588 } 589 return focusModel; 590 } 591 592 593 // --- Expanded node count 594 /** 595 * <p>Represents the number of tree nodes presently able to be visible in the 596 * TreeView. This is essentially the count of all expanded tree items, and 597 * their children. 598 * 599 * <p>For example, if just the root node is visible, the expandedItemCount will 600 * be one. If the root had three children and the root was expanded, the value 601 * will be four. 602 * @since JavaFX 8.0 603 */ 604 private ReadOnlyIntegerWrapper expandedItemCount = new ReadOnlyIntegerWrapper(this, "expandedItemCount", 0); 605 public final ReadOnlyIntegerProperty expandedItemCountProperty() { 606 return expandedItemCount.getReadOnlyProperty(); 607 } 608 private void setExpandedItemCount(int value) { 609 expandedItemCount.set(value); 610 } 611 public final int getExpandedItemCount() { 612 if (expandedItemCountDirty) { 613 updateExpandedItemCount(getRoot()); 614 } 615 return expandedItemCount.get(); 616 } 617 618 619 // --- Fixed cell size 620 private DoubleProperty fixedCellSize; 621 622 /** 623 * Sets the new fixed cell size for this control. Any value greater than 624 * zero will enable fixed cell size mode, whereas a zero or negative value 625 * (or Region.USE_COMPUTED_SIZE) will be used to disabled fixed cell size 626 * mode. 627 * 628 * @param value The new fixed cell size value, or a value less than or equal 629 * to zero (or Region.USE_COMPUTED_SIZE) to disable. 630 * @since JavaFX 8.0 631 */ 632 public final void setFixedCellSize(double value) { 633 fixedCellSizeProperty().set(value); 634 } 635 636 /** 637 * Returns the fixed cell size value. A value less than or equal to zero is 638 * used to represent that fixed cell size mode is disabled, and a value 639 * greater than zero represents the size of all cells in this control. 640 * 641 * @return A double representing the fixed cell size of this control, or a 642 * value less than or equal to zero if fixed cell size mode is disabled. 643 * @since JavaFX 8.0 644 */ 645 public final double getFixedCellSize() { 646 return fixedCellSize == null ? Region.USE_COMPUTED_SIZE : fixedCellSize.get(); 647 } 648 /** 649 * Specifies whether this control has cells that are a fixed height (of the 650 * specified value). If this value is less than or equal to zero, 651 * then all cells are individually sized and positioned. This is a slow 652 * operation. Therefore, when performance matters and developers are not 653 * dependent on variable cell sizes it is a good idea to set the fixed cell 654 * size value. Generally cells are around 24px, so setting a fixed cell size 655 * of 24 is likely to result in very little difference in visuals, but a 656 * improvement to performance. 657 * 658 * <p>To set this property via CSS, use the -fx-fixed-cell-size property. 659 * This should not be confused with the -fx-cell-size property. The difference 660 * between these two CSS properties is that -fx-cell-size will size all 661 * cells to the specified size, but it will not enforce that this is the 662 * only size (thus allowing for variable cell sizes, and preventing the 663 * performance gains from being possible). Therefore, when performance matters 664 * use -fx-fixed-cell-size, instead of -fx-cell-size. If both properties are 665 * specified in CSS, -fx-fixed-cell-size takes precedence.</p> 666 * 667 * @since JavaFX 8.0 668 */ 669 public final DoubleProperty fixedCellSizeProperty() { 670 if (fixedCellSize == null) { 671 fixedCellSize = new StyleableDoubleProperty(Region.USE_COMPUTED_SIZE) { 672 @Override public CssMetaData<TreeView<?>,Number> getCssMetaData() { 673 return StyleableProperties.FIXED_CELL_SIZE; 674 } 675 676 @Override public Object getBean() { 677 return TreeView.this; 678 } 679 680 @Override public String getName() { 681 return "fixedCellSize"; 682 } 683 }; 684 } 685 return fixedCellSize; 686 } 687 688 689 // --- Editable 690 private BooleanProperty editable; 691 public final void setEditable(boolean value) { 692 editableProperty().set(value); 693 } 694 public final boolean isEditable() { 695 return editable == null ? false : editable.get(); 696 } 697 /** 698 * Specifies whether this TreeView is editable - only if the TreeView and 699 * the TreeCells within it are both editable will a TreeCell be able to go 700 * into their editing state. 701 */ 702 public final BooleanProperty editableProperty() { 703 if (editable == null) { 704 editable = new SimpleBooleanProperty(this, "editable", false); 705 } 706 return editable; 707 } 708 709 710 // --- Editing Item 711 private ReadOnlyObjectWrapper<TreeItem<T>> editingItem; 712 713 private void setEditingItem(TreeItem<T> value) { 714 editingItemPropertyImpl().set(value); 715 } 716 717 /** 718 * Returns the TreeItem that is currently being edited in the TreeView, 719 * or null if no item is being edited. 720 */ 721 public final TreeItem<T> getEditingItem() { 722 return editingItem == null ? null : editingItem.get(); 723 } 724 725 /** 726 * <p>A property used to represent the TreeItem currently being edited 727 * in the TreeView, if editing is taking place, or null if no item is being edited. 728 * 729 * <p>It is not possible to set the editing item, instead it is required that 730 * you call {@link #edit(javafx.scene.control.TreeItem)}. 731 */ 732 public final ReadOnlyObjectProperty<TreeItem<T>> editingItemProperty() { 733 return editingItemPropertyImpl().getReadOnlyProperty(); 734 } 735 736 private ReadOnlyObjectWrapper<TreeItem<T>> editingItemPropertyImpl() { 737 if (editingItem == null) { 738 editingItem = new ReadOnlyObjectWrapper<TreeItem<T>>(this, "editingItem"); 739 } 740 return editingItem; 741 } 742 743 744 // --- On Edit Start 745 private ObjectProperty<EventHandler<EditEvent<T>>> onEditStart; 746 747 /** 748 * Sets the {@link EventHandler} that will be called when the user begins 749 * an edit. 750 */ 751 public final void setOnEditStart(EventHandler<EditEvent<T>> value) { 752 onEditStartProperty().set(value); 753 } 754 755 /** 756 * Returns the {@link EventHandler} that will be called when the user begins 757 * an edit. 758 */ 759 public final EventHandler<EditEvent<T>> getOnEditStart() { 760 return onEditStart == null ? null : onEditStart.get(); 761 } 762 763 /** 764 * This event handler will be fired when the user successfully initiates 765 * editing. 766 */ 767 public final ObjectProperty<EventHandler<EditEvent<T>>> onEditStartProperty() { 768 if (onEditStart == null) { 769 onEditStart = new SimpleObjectProperty<EventHandler<EditEvent<T>>>(this, "onEditStart") { 770 @Override protected void invalidated() { 771 setEventHandler(TreeView.<T>editStartEvent(), get()); 772 } 773 }; 774 } 775 return onEditStart; 776 } 777 778 779 // --- On Edit Commit 780 private ObjectProperty<EventHandler<EditEvent<T>>> onEditCommit; 781 782 /** 783 * Sets the {@link EventHandler} that will be called when the user commits 784 * an edit. 785 */ 786 public final void setOnEditCommit(EventHandler<EditEvent<T>> value) { 787 onEditCommitProperty().set(value); 788 } 789 790 /** 791 * Returns the {@link EventHandler} that will be called when the user commits 792 * an edit. 793 */ 794 public final EventHandler<EditEvent<T>> getOnEditCommit() { 795 return onEditCommit == null ? null : onEditCommit.get(); 796 } 797 798 /** 799 * <p>This property is used when the user performs an action that should 800 * result in their editing input being persisted.</p> 801 * 802 * <p>The EventHandler in this property should not be called directly - 803 * instead call {@link TreeCell#commitEdit(java.lang.Object)} from within 804 * your custom TreeCell. This will handle firing this event, updating the 805 * view, and switching out of the editing state.</p> 806 */ 807 public final ObjectProperty<EventHandler<EditEvent<T>>> onEditCommitProperty() { 808 if (onEditCommit == null) { 809 onEditCommit = new SimpleObjectProperty<EventHandler<EditEvent<T>>>(this, "onEditCommit") { 810 @Override protected void invalidated() { 811 setEventHandler(TreeView.<T>editCommitEvent(), get()); 812 } 813 }; 814 } 815 return onEditCommit; 816 } 817 818 819 // --- On Edit Cancel 820 private ObjectProperty<EventHandler<EditEvent<T>>> onEditCancel; 821 822 /** 823 * Sets the {@link EventHandler} that will be called when the user cancels 824 * an edit. 825 */ 826 public final void setOnEditCancel(EventHandler<EditEvent<T>> value) { 827 onEditCancelProperty().set(value); 828 } 829 830 /** 831 * Returns the {@link EventHandler} that will be called when the user cancels 832 * an edit. 833 */ 834 public final EventHandler<EditEvent<T>> getOnEditCancel() { 835 return onEditCancel == null ? null : onEditCancel.get(); 836 } 837 838 /** 839 * This event handler will be fired when the user cancels editing a cell. 840 */ 841 public final ObjectProperty<EventHandler<EditEvent<T>>> onEditCancelProperty() { 842 if (onEditCancel == null) { 843 onEditCancel = new SimpleObjectProperty<EventHandler<EditEvent<T>>>(this, "onEditCancel") { 844 @Override protected void invalidated() { 845 setEventHandler(TreeView.<T>editCancelEvent(), get()); 846 } 847 }; 848 } 849 return onEditCancel; 850 } 851 852 853 854 /*************************************************************************** 855 * * 856 * Public API * 857 * * 858 **************************************************************************/ 859 860 861 /** {@inheritDoc} */ 862 @Override protected void layoutChildren() { 863 if (expandedItemCountDirty) { 864 updateExpandedItemCount(getRoot()); 865 } 866 867 super.layoutChildren(); 868 } 869 870 871 /** 872 * Instructs the TreeView to begin editing the given TreeItem, if 873 * the TreeView is {@link #editableProperty() editable}. Once 874 * this method is called, if the current 875 * {@link #cellFactoryProperty() cell factory} is set up to support editing, 876 * the Cell will switch its visual state to enable the user input to take place. 877 * 878 * @param item The TreeItem in the TreeView that should be edited. 879 */ 880 public void edit(TreeItem<T> item) { 881 if (!isEditable()) return; 882 setEditingItem(item); 883 } 884 885 886 /** 887 * Scrolls the TreeView such that the item in the given index is visible to 888 * the end user. 889 * 890 * @param index The index that should be made visible to the user, assuming 891 * of course that it is greater than, or equal to 0, and less than the 892 * number of the visible items in the TreeView. 893 */ 894 public void scrollTo(int index) { 895 ControlUtils.scrollToIndex(this, index); 896 } 897 898 /** 899 * Called when there's a request to scroll an index into view using {@link #scrollTo(int)} 900 * @since JavaFX 8.0 901 */ 902 private ObjectProperty<EventHandler<ScrollToEvent<Integer>>> onScrollTo; 903 904 public void setOnScrollTo(EventHandler<ScrollToEvent<Integer>> value) { 905 onScrollToProperty().set(value); 906 } 907 908 public EventHandler<ScrollToEvent<Integer>> getOnScrollTo() { 909 if( onScrollTo != null ) { 910 return onScrollTo.get(); 911 } 912 return null; 913 } 914 915 public ObjectProperty<EventHandler<ScrollToEvent<Integer>>> onScrollToProperty() { 916 if( onScrollTo == null ) { 917 onScrollTo = new ObjectPropertyBase<EventHandler<ScrollToEvent<Integer>>>() { 918 @Override 919 protected void invalidated() { 920 setEventHandler(ScrollToEvent.scrollToTopIndex(), get()); 921 } 922 @Override 923 public Object getBean() { 924 return TreeView.this; 925 } 926 927 @Override 928 public String getName() { 929 return "onScrollTo"; 930 } 931 }; 932 } 933 return onScrollTo; 934 } 935 936 /** 937 * Returns the index position of the given TreeItem, assuming that it is 938 * currently accessible through the tree hierarchy (most notably, that all 939 * parent tree items are expanded). If a parent tree item is collapsed, 940 * the result is that this method will return -1 to indicate that the 941 * given tree item is not accessible in the tree. 942 * 943 * @param item The TreeItem for which the index is sought. 944 * @return An integer representing the location in the current TreeView of the 945 * first instance of the given TreeItem, or -1 if it is null or can not 946 * be found (for example, if a parent (all the way up to the root) is 947 * collapsed). 948 */ 949 public int getRow(TreeItem<T> item) { 950 return TreeUtil.getRow(item, getRoot(), expandedItemCountDirty, isShowRoot()); 951 } 952 953 /** 954 * Returns the TreeItem in the given index, or null if it is out of bounds. 955 * 956 * @param row The index of the TreeItem being sought. 957 * @return The TreeItem in the given index, or null if it is out of bounds. 958 */ 959 public TreeItem<T> getTreeItem(int row) { 960 if (row < 0) return null; 961 962 // normalize the requested row based on whether showRoot is set 963 final int _row = isShowRoot() ? row : (row + 1); 964 965 if (expandedItemCountDirty) { 966 updateExpandedItemCount(getRoot()); 967 } else { 968 if (treeItemCacheMap.containsKey(_row)) { 969 SoftReference<TreeItem<T>> treeItemRef = treeItemCacheMap.get(_row); 970 TreeItem<T> treeItem = treeItemRef.get(); 971 if (treeItem != null) { 972 return treeItem; 973 } 974 } 975 } 976 977 TreeItem<T> treeItem = TreeUtil.getItem(getRoot(), _row, expandedItemCountDirty); 978 treeItemCacheMap.put(_row, new SoftReference<>(treeItem)); 979 return treeItem; 980 } 981 982 /** 983 * Returns the number of levels of 'indentation' of the given TreeItem, 984 * based on how many times getParent() can be recursively called. If the 985 * given TreeItem is the root node of this TreeView, or if the TreeItem does 986 * not have any parent set, the returned value will be zero. For each time 987 * getParent() is recursively called, the returned value is incremented by one. 988 * 989 * @param node The TreeItem for which the level is needed. 990 * @return An integer representing the number of parents above the given node, 991 * or -1 if the given TreeItem is null. 992 */ 993 public int getTreeItemLevel(TreeItem<?> node) { 994 final TreeItem<?> root = getRoot(); 995 996 if (node == null) return -1; 997 if (node == root) return 0; 998 999 int level = 0; 1000 TreeItem<?> parent = node.getParent(); 1001 while (parent != null) { 1002 level++; 1003 1004 if (parent == root) { 1005 break; 1006 } 1007 1008 parent = parent.getParent(); 1009 } 1010 1011 return level; 1012 } 1013 1014 /** {@inheritDoc} */ 1015 @Override protected Skin<?> createDefaultSkin() { 1016 return new TreeViewSkin<T>(this); 1017 } 1018 1019 /** 1020 * Calling {@code refresh()} forces the TreeView control to recreate and 1021 * repopulate the cells necessary to populate the visual bounds of the control. 1022 * In other words, this forces the TreeView to update what it is showing to 1023 * the user. This is useful in cases where the underlying data source has 1024 * changed in a way that is not observed by the TreeView itself. 1025 * 1026 * @since JavaFX 8u60 1027 */ 1028 public void refresh() { 1029 getProperties().put(Properties.RECREATE, Boolean.TRUE); 1030 } 1031 1032 1033 1034 /*************************************************************************** 1035 * * 1036 * Private Implementation * 1037 * * 1038 **************************************************************************/ 1039 1040 private void updateExpandedItemCount(TreeItem<T> treeItem) { 1041 setExpandedItemCount(TreeUtil.updateExpandedItemCount(treeItem, expandedItemCountDirty, isShowRoot())); 1042 1043 if (expandedItemCountDirty) { 1044 // this is a very inefficient thing to do, but for now having a cache 1045 // is better than nothing at all... 1046 treeItemCacheMap.clear(); 1047 } 1048 1049 expandedItemCountDirty = false; 1050 } 1051 1052 private void updateRootExpanded() { 1053 // if we aren't showing the root, and the root isn't expanded, we expand 1054 // it now so that something is shown. 1055 if (!isShowRoot() && getRoot() != null && ! getRoot().isExpanded()) { 1056 getRoot().setExpanded(true); 1057 } 1058 } 1059 1060 1061 1062 /*************************************************************************** 1063 * * 1064 * Stylesheet Handling * 1065 * * 1066 **************************************************************************/ 1067 1068 private static final String DEFAULT_STYLE_CLASS = "tree-view"; 1069 1070 private static class StyleableProperties { 1071 private static final CssMetaData<TreeView<?>,Number> FIXED_CELL_SIZE = 1072 new CssMetaData<TreeView<?>,Number>("-fx-fixed-cell-size", 1073 SizeConverter.getInstance(), 1074 Region.USE_COMPUTED_SIZE) { 1075 1076 @Override public Double getInitialValue(TreeView<?> node) { 1077 return node.getFixedCellSize(); 1078 } 1079 1080 @Override public boolean isSettable(TreeView<?> n) { 1081 return n.fixedCellSize == null || !n.fixedCellSize.isBound(); 1082 } 1083 1084 @Override public StyleableProperty<Number> getStyleableProperty(TreeView<?> n) { 1085 return (StyleableProperty<Number>)(WritableValue<Number>) n.fixedCellSizeProperty(); 1086 } 1087 }; 1088 1089 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 1090 static { 1091 final List<CssMetaData<? extends Styleable, ?>> styleables = 1092 new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData()); 1093 styleables.add(FIXED_CELL_SIZE); 1094 STYLEABLES = Collections.unmodifiableList(styleables); 1095 } 1096 } 1097 1098 /** 1099 * @return The CssMetaData associated with this class, which may include the 1100 * CssMetaData of its superclasses. 1101 * @since JavaFX 8.0 1102 */ 1103 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 1104 return StyleableProperties.STYLEABLES; 1105 } 1106 1107 /** 1108 * {@inheritDoc} 1109 * @since JavaFX 8.0 1110 */ 1111 @Override 1112 public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() { 1113 return getClassCssMetaData(); 1114 } 1115 1116 1117 1118 /*************************************************************************** 1119 * * 1120 * Accessibility handling * 1121 * * 1122 **************************************************************************/ 1123 1124 @Override 1125 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 1126 switch (attribute) { 1127 case MULTIPLE_SELECTION: { 1128 MultipleSelectionModel<TreeItem<T>> sm = getSelectionModel(); 1129 return sm != null && sm.getSelectionMode() == SelectionMode.MULTIPLE; 1130 } 1131 case ROW_COUNT: return getExpandedItemCount(); 1132 default: return super.queryAccessibleAttribute(attribute, parameters); 1133 } 1134 } 1135 1136 1137 1138 /*************************************************************************** 1139 * * 1140 * Support Interfaces * 1141 * * 1142 **************************************************************************/ 1143 1144 1145 1146 /*************************************************************************** 1147 * * 1148 * Support Classes * 1149 * * 1150 **************************************************************************/ 1151 1152 1153 /** 1154 * An {@link Event} subclass used specifically in TreeView for representing 1155 * edit-related events. It provides additional API to easily access the 1156 * TreeItem that the edit event took place on, as well as the input provided 1157 * by the end user. 1158 * 1159 * @param <T> The type of the input, which is the same type as the TreeView 1160 * itself. 1161 * @since JavaFX 2.0 1162 */ 1163 public static class EditEvent<T> extends Event { 1164 private static final long serialVersionUID = -4437033058917528976L; 1165 1166 /** 1167 * Common supertype for all edit event types. 1168 * @since JavaFX 8.0 1169 */ 1170 public static final EventType<?> ANY = EDIT_ANY_EVENT; 1171 1172 private final TreeView<T> source; 1173 private final T oldValue; 1174 private final T newValue; 1175 private transient final TreeItem<T> treeItem; 1176 1177 /** 1178 * Creates a new EditEvent instance to represent an edit event. This 1179 * event is used for {@link #EDIT_START_EVENT}, 1180 * {@link #EDIT_COMMIT_EVENT} and {@link #EDIT_CANCEL_EVENT} types. 1181 */ 1182 public EditEvent(TreeView<T> source, 1183 EventType<? extends EditEvent> eventType, 1184 TreeItem<T> treeItem, T oldValue, T newValue) { 1185 super(source, Event.NULL_SOURCE_TARGET, eventType); 1186 this.source = source; 1187 this.oldValue = oldValue; 1188 this.newValue = newValue; 1189 this.treeItem = treeItem; 1190 } 1191 1192 /** 1193 * Returns the TreeView upon which the edit took place. 1194 */ 1195 @Override public TreeView<T> getSource() { 1196 return source; 1197 } 1198 1199 /** 1200 * Returns the {@link TreeItem} upon which the edit took place. 1201 */ 1202 public TreeItem<T> getTreeItem() { 1203 return treeItem; 1204 } 1205 1206 /** 1207 * Returns the new value input into the TreeItem by the end user. 1208 */ 1209 public T getNewValue() { 1210 return newValue; 1211 } 1212 1213 /** 1214 * Returns the old value that existed in the TreeItem prior to the current 1215 * edit event. 1216 */ 1217 public T getOldValue() { 1218 return oldValue; 1219 } 1220 } 1221 1222 1223 1224 1225 1226 1227 1228 // package for testing 1229 static class TreeViewBitSetSelectionModel<T> extends MultipleSelectionModelBase<TreeItem<T>> { 1230 1231 /*********************************************************************** 1232 * * 1233 * Internal fields * 1234 * * 1235 **********************************************************************/ 1236 1237 private TreeView<T> treeView = null; 1238 1239 1240 1241 /*********************************************************************** 1242 * * 1243 * Constructors * 1244 * * 1245 **********************************************************************/ 1246 1247 public TreeViewBitSetSelectionModel(final TreeView<T> treeView) { 1248 if (treeView == null) { 1249 throw new IllegalArgumentException("TreeView can not be null"); 1250 } 1251 1252 this.treeView = treeView; 1253 this.treeView.rootProperty().addListener(weakRootPropertyListener); 1254 this.treeView.showRootProperty().addListener(o -> { 1255 shiftSelection(0, treeView.isShowRoot() ? 1 : -1, null); 1256 }); 1257 1258 updateTreeEventListener(null, treeView.getRoot()); 1259 1260 updateDefaultSelection(); 1261 } 1262 1263 private void updateTreeEventListener(TreeItem<T> oldRoot, TreeItem<T> newRoot) { 1264 if (oldRoot != null && weakTreeItemListener != null) { 1265 oldRoot.removeEventHandler(TreeItem.<T>expandedItemCountChangeEvent(), weakTreeItemListener); 1266 } 1267 1268 if (newRoot != null) { 1269 weakTreeItemListener = new WeakEventHandler<>(treeItemListener); 1270 newRoot.addEventHandler(TreeItem.<T>expandedItemCountChangeEvent(), weakTreeItemListener); 1271 } 1272 } 1273 1274 private ChangeListener<TreeItem<T>> rootPropertyListener = (observable, oldValue, newValue) -> { 1275 updateDefaultSelection(); 1276 updateTreeEventListener(oldValue, newValue); 1277 }; 1278 1279 private EventHandler<TreeModificationEvent<T>> treeItemListener = e -> { 1280 if (getSelectedIndex() == -1 && getSelectedItem() == null) return; 1281 1282 final TreeItem<T> treeItem = e.getTreeItem(); 1283 if (treeItem == null) return; 1284 1285 treeView.expandedItemCountDirty = true; 1286 1287 // we only shift selection from this row - everything before it 1288 // is safe. We might change this below based on certain criteria 1289 int startRow = treeView.getRow(treeItem); 1290 1291 int shift = 0; 1292 ListChangeListener.Change<? extends TreeItem<?>> change = e.getChange(); 1293 if (change != null) { 1294 change.next(); 1295 } 1296 1297 do { 1298 final int addedSize = change == null ? 0 : change.getAddedSize(); 1299 final int removedSize = change == null ? 0 : change.getRemovedSize(); 1300 1301 if (e.wasExpanded()) { 1302 // need to shuffle selection by the number of visible children 1303 shift += treeItem.getExpandedDescendentCount(false) - 1; 1304 startRow++; 1305 } else if (e.wasCollapsed()) { 1306 // remove selection from any child treeItem, and also determine 1307 // if any child item was selected (in which case the parent 1308 // takes the selection on collapse) 1309 treeItem.getExpandedDescendentCount(false); 1310 final int count = treeItem.previousExpandedDescendentCount; 1311 1312 final int selectedIndex = getSelectedIndex(); 1313 final boolean wasPrimarySelectionInChild = 1314 selectedIndex >= (startRow + 1) && 1315 selectedIndex < (startRow + count); 1316 1317 boolean wasAnyChildSelected = false; 1318 1319 selectedIndices._beginChange(); 1320 final int from = startRow + 1; 1321 final int to = startRow + count; 1322 final List<Integer> removed = new ArrayList<>(); 1323 for (int i = from; i < to; i++) { 1324 if (isSelected(i)) { 1325 wasAnyChildSelected = true; 1326 removed.add(i); 1327 } 1328 } 1329 1330 ControlUtils.reducingChange(selectedIndices, removed); 1331 1332 for (int index : removed) { 1333 startAtomic(); 1334 clearSelection(index); 1335 stopAtomic(); 1336 } 1337 selectedIndices._endChange(); 1338 1339 // put selection onto the newly-collapsed tree item 1340 if (wasPrimarySelectionInChild && wasAnyChildSelected) { 1341 select(startRow); 1342 } 1343 1344 shift += -count + 1; 1345 startRow++; 1346 } else if (e.wasPermutated()) { 1347 // no-op 1348 } else if (e.wasAdded()) { 1349 // shuffle selection by the number of added items 1350 shift += treeItem.isExpanded() ? addedSize : 0; 1351 1352 // RT-32963: We were taking the startRow from the TreeItem 1353 // in which the children were added, rather than from the 1354 // actual position of the new child. This led to selection 1355 // being moved off the parent TreeItem by mistake. 1356 // The 'if (e.getAddedSize() == 1)' condition here was 1357 // subsequently commented out due to RT-33894. 1358 startRow = treeView.getRow(e.getChange().getAddedSubList().get(0)); 1359 } else if (e.wasRemoved()) { 1360 // shuffle selection by the number of removed items 1361 shift += treeItem.isExpanded() ? -removedSize : 0; 1362 1363 // the start row is incorrect - it is _not_ the index of the 1364 // TreeItem in which the children were removed from (which is 1365 // what it currently represents). We need to take the 'from' 1366 // value out of the event and make use of that to understand 1367 // what actually changed inside the children list. 1368 startRow += e.getFrom() + 1; 1369 1370 // whilst we are here, we should check if the removed items 1371 // are part of the selectedItems list - and remove them 1372 // from selection if they are (as per RT-15446) 1373 final List<Integer> selectedIndices1 = getSelectedIndices(); 1374 final int selectedIndex = getSelectedIndex(); 1375 final List<TreeItem<T>> selectedItems = getSelectedItems(); 1376 final TreeItem<T> selectedItem = getSelectedItem(); 1377 final List<? extends TreeItem<T>> removedChildren = e.getChange().getRemoved(); 1378 1379 for (int i = 0; i < selectedIndices1.size() && !selectedItems.isEmpty(); i++) { 1380 int index = selectedIndices1.get(i); 1381 if (index > selectedItems.size()) break; 1382 1383 if (removedChildren.size() == 1 && 1384 selectedItems.size() == 1 && 1385 selectedItem != null && 1386 selectedItem.equals(removedChildren.get(0))) { 1387 // Bug fix for RT-28637 1388 if (selectedIndex < getItemCount()) { 1389 final int previousRow = selectedIndex == 0 ? 0 : selectedIndex - 1; 1390 TreeItem<T> newSelectedItem = getModelItem(previousRow); 1391 if (!selectedItem.equals(newSelectedItem)) { 1392 select(newSelectedItem); 1393 } 1394 } 1395 } 1396 } 1397 } 1398 } while (e.getChange() != null && e.getChange().next()); 1399 1400 shiftSelection(startRow, shift, null); 1401 1402 if (e.wasAdded() || e.wasRemoved()) { 1403 Integer anchor = TreeCellBehavior.getAnchor(treeView, null); 1404 if (anchor != null && isSelected(anchor + shift)) { 1405 TreeCellBehavior.setAnchor(treeView, anchor + shift, false); 1406 } 1407 } 1408 }; 1409 1410 private WeakChangeListener<TreeItem<T>> weakRootPropertyListener = 1411 new WeakChangeListener<>(rootPropertyListener); 1412 1413 private WeakEventHandler<TreeModificationEvent<T>> weakTreeItemListener; 1414 1415 1416 1417 /*********************************************************************** 1418 * * 1419 * Public selection API * 1420 * * 1421 **********************************************************************/ 1422 1423 /** {@inheritDoc} */ 1424 @Override public void selectAll() { 1425 // when a selectAll happens, the anchor should not change, so we store it 1426 // before, and restore it afterwards 1427 final int anchor = TreeCellBehavior.getAnchor(treeView, -1); 1428 super.selectAll(); 1429 TreeCellBehavior.setAnchor(treeView, anchor, false); 1430 } 1431 1432 /** {@inheritDoc} */ 1433 @Override public void select(TreeItem<T> obj) { 1434 // if (getRowCount() <= 0) return; 1435 1436 if (obj == null && getSelectionMode() == SelectionMode.SINGLE) { 1437 clearSelection(); 1438 return; 1439 } 1440 1441 // we firstly expand the path down such that the given object is 1442 // visible. This fixes RT-14456, where selection was not happening 1443 // correctly on TreeItems that are not visible. 1444 1445 if (obj != null) { 1446 TreeItem<?> item = obj.getParent(); 1447 while (item != null) { 1448 item.setExpanded(true); 1449 item = item.getParent(); 1450 } 1451 } 1452 1453 // Fix for RT-15419. We eagerly update the tree item count, such that 1454 // selection occurs on the row 1455 treeView.updateExpandedItemCount(treeView.getRoot()); 1456 1457 // We have no option but to iterate through the model and select the 1458 // first occurrence of the given object. Once we find the first one, we 1459 // don't proceed to select any others. 1460 int row = treeView.getRow(obj); 1461 1462 if (row == -1) { 1463 // if we are here, we did not find the item in the entire data model. 1464 // Even still, we allow for this item to be set to the give object. 1465 // We expect that in concrete subclasses of this class we observe the 1466 // data model such that we check to see if the given item exists in it, 1467 // whilst SelectedIndex == -1 && SelectedItem != null. 1468 setSelectedIndex(-1); 1469 setSelectedItem(obj); 1470 } else { 1471 select(row); 1472 } 1473 } 1474 1475 /** {@inheritDoc} */ 1476 @Override public void clearAndSelect(int row) { 1477 TreeCellBehavior.setAnchor(treeView, row, false); 1478 super.clearAndSelect(row); 1479 } 1480 1481 1482 /*********************************************************************** 1483 * * 1484 * Support code * 1485 * * 1486 **********************************************************************/ 1487 1488 /** {@inheritDoc} */ 1489 @Override protected void focus(int itemIndex) { 1490 if (treeView.getFocusModel() != null) { 1491 treeView.getFocusModel().focus(itemIndex); 1492 } 1493 1494 // FIXME this is not the correct location for fire selection events (and does not take into account multiple selection) 1495 treeView.notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM); 1496 } 1497 1498 /** {@inheritDoc} */ 1499 @Override protected int getFocusedIndex() { 1500 if (treeView.getFocusModel() == null) return -1; 1501 return treeView.getFocusModel().getFocusedIndex(); 1502 } 1503 1504 /** {@inheritDoc} */ 1505 @Override protected int getItemCount() { 1506 return treeView == null ? 0 : treeView.getExpandedItemCount(); 1507 } 1508 1509 /** {@inheritDoc} */ 1510 @Override public TreeItem<T> getModelItem(int index) { 1511 if (treeView == null) return null; 1512 1513 if (index < 0 || index >= treeView.getExpandedItemCount()) return null; 1514 1515 return treeView.getTreeItem(index); 1516 } 1517 1518 1519 1520 /*********************************************************************** 1521 * * 1522 * Private implementation * 1523 * * 1524 **********************************************************************/ 1525 1526 private void updateDefaultSelection() { 1527 clearSelection(); 1528 1529 // we put focus onto the first item, if there is at least 1530 // one item in the list 1531 focus(getItemCount() > 0 ? 0 : -1); 1532 } 1533 } 1534 1535 1536 1537 /** 1538 * 1539 * @param <T> 1540 */ 1541 static class TreeViewFocusModel<T> extends FocusModel<TreeItem<T>> { 1542 1543 private final TreeView<T> treeView; 1544 1545 public TreeViewFocusModel(final TreeView<T> treeView) { 1546 this.treeView = treeView; 1547 this.treeView.rootProperty().addListener(weakRootPropertyListener); 1548 updateTreeEventListener(null, treeView.getRoot()); 1549 1550 if (treeView.getExpandedItemCount() > 0) { 1551 focus(0); 1552 } 1553 1554 treeView.showRootProperty().addListener(o -> { 1555 if (isFocused(0)) { 1556 focus(-1); 1557 focus(0); 1558 } 1559 }); 1560 1561 focusedIndexProperty().addListener(o -> { 1562 treeView.notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM); 1563 }); 1564 } 1565 1566 private final ChangeListener<TreeItem<T>> rootPropertyListener = (observable, oldValue, newValue) -> { 1567 updateTreeEventListener(oldValue, newValue); 1568 }; 1569 1570 private final WeakChangeListener<TreeItem<T>> weakRootPropertyListener = 1571 new WeakChangeListener<>(rootPropertyListener); 1572 1573 private void updateTreeEventListener(TreeItem<T> oldRoot, TreeItem<T> newRoot) { 1574 if (oldRoot != null && weakTreeItemListener != null) { 1575 oldRoot.removeEventHandler(TreeItem.<T>expandedItemCountChangeEvent(), weakTreeItemListener); 1576 } 1577 1578 if (newRoot != null) { 1579 weakTreeItemListener = new WeakEventHandler<>(treeItemListener); 1580 newRoot.addEventHandler(TreeItem.<T>expandedItemCountChangeEvent(), weakTreeItemListener); 1581 } 1582 } 1583 1584 private EventHandler<TreeModificationEvent<T>> treeItemListener = new EventHandler<TreeModificationEvent<T>>() { 1585 @Override public void handle(TreeModificationEvent<T> e) { 1586 // don't shift focus if the event occurred on a tree item after 1587 // the focused row, or if there is no focus index at present 1588 if (getFocusedIndex() == -1) return; 1589 1590 int row = treeView.getRow(e.getTreeItem()); 1591 1592 int shift = 0; 1593 if (e.getChange() != null) { 1594 e.getChange().next(); 1595 } 1596 1597 do { 1598 if (e.wasExpanded()) { 1599 if (row < getFocusedIndex()) { 1600 // need to shuffle selection by the number of visible children 1601 shift += e.getTreeItem().getExpandedDescendentCount(false) - 1; 1602 } 1603 } else if (e.wasCollapsed()) { 1604 if (row < getFocusedIndex()) { 1605 // need to shuffle selection by the number of visible children 1606 // that were just hidden 1607 shift += -e.getTreeItem().previousExpandedDescendentCount + 1; 1608 } 1609 } else if (e.wasAdded()) { 1610 // get the TreeItem the event occurred on - we only need to 1611 // shift if the tree item is expanded 1612 TreeItem<T> eventTreeItem = e.getTreeItem(); 1613 if (eventTreeItem.isExpanded()) { 1614 for (int i = 0; i < e.getAddedChildren().size(); i++) { 1615 // get the added item and determine the row it is in 1616 TreeItem<T> item = e.getAddedChildren().get(i); 1617 row = treeView.getRow(item); 1618 1619 if (item != null && row <= (shift+getFocusedIndex())) { 1620 shift += item.getExpandedDescendentCount(false); 1621 } 1622 } 1623 } 1624 } else if (e.wasRemoved()) { 1625 row += e.getFrom() + 1; 1626 1627 for (int i = 0; i < e.getRemovedChildren().size(); i++) { 1628 TreeItem<T> item = e.getRemovedChildren().get(i); 1629 if (item != null && item.equals(getFocusedItem())) { 1630 focus(Math.max(0, getFocusedIndex() - 1)); 1631 return; 1632 } 1633 } 1634 1635 if (row <= getFocusedIndex()) { 1636 // shuffle selection by the number of removed items 1637 shift += e.getTreeItem().isExpanded() ? -e.getRemovedSize() : 0; 1638 } 1639 } 1640 } while (e.getChange() != null && e.getChange().next()); 1641 1642 if(shift != 0) { 1643 final int newFocus = getFocusedIndex() + shift; 1644 if (newFocus >= 0) { 1645 Platform.runLater(() -> focus(newFocus)); 1646 } 1647 } 1648 } 1649 }; 1650 1651 private WeakEventHandler<TreeModificationEvent<T>> weakTreeItemListener; 1652 1653 @Override protected int getItemCount() { 1654 return treeView == null ? -1 : treeView.getExpandedItemCount(); 1655 } 1656 1657 @Override protected TreeItem<T> getModelItem(int index) { 1658 if (treeView == null) return null; 1659 1660 if (index < 0 || index >= treeView.getExpandedItemCount()) return null; 1661 1662 return treeView.getTreeItem(index); 1663 } 1664 1665 /** {@inheritDoc} */ 1666 @Override public void focus(int index) { 1667 if (treeView.expandedItemCountDirty) { 1668 treeView.updateExpandedItemCount(treeView.getRoot()); 1669 } 1670 1671 super.focus(index); 1672 } 1673 } 1674 }