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