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