Spec-Zone .ru
спецификации, руководства, описания, API
|
001/* 002 * Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved. 003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 004 * 005 * This code is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU General Public License version 2 only, as 007 * published by the Free Software Foundation. Oracle designates this 008 * particular file as subject to the "Classpath" exception as provided 009 * by Oracle in the LICENSE file that accompanied this code. 010 * 011 * This code is distributed in the hope that it will be useful, but WITHOUT 012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 014 * version 2 for more details (a copy is included in the LICENSE file that 015 * accompanied this code). 016 * 017 * You should have received a copy of the GNU General Public License version 018 * 2 along with this work; if not, write to the Free Software Foundation, 019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 020 * 021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 022 * or visit www.oracle.com if you need additional information or have any 023 * questions. 024 */ 025 026package javafx.scene.control; 027 028import java.util.ArrayList; 029import java.util.Collections; 030import java.util.List; 031 032import javafx.beans.property.BooleanProperty; 033import javafx.beans.property.DoubleProperty; 034import javafx.beans.property.IntegerProperty; 035import javafx.beans.property.ObjectProperty; 036import javafx.beans.property.ObjectPropertyBase; 037import javafx.beans.property.SimpleDoubleProperty; 038import javafx.beans.property.SimpleIntegerProperty; 039import javafx.beans.property.SimpleObjectProperty; 040import javafx.beans.value.ChangeListener; 041import javafx.beans.value.ObservableValue; 042import javafx.css.CssMetaData; 043import javafx.css.Styleable; 044import javafx.css.StyleableDoubleProperty; 045import javafx.css.StyleableProperty; 046import javafx.event.Event; 047import javafx.event.EventHandler; 048import javafx.event.EventType; 049import javafx.scene.control.TreeItem.TreeModificationEvent; 050import javafx.scene.layout.Region; 051import javafx.util.Callback; 052 053import javafx.event.WeakEventHandler; 054import com.sun.javafx.css.converters.SizeConverter; 055import com.sun.javafx.scene.control.skin.TreeViewSkin; 056import com.sun.javafx.scene.control.skin.VirtualContainerBase; 057import java.lang.ref.WeakReference; 058import javafx.application.Platform; 059import javafx.beans.DefaultProperty; 060import javafx.beans.property.ReadOnlyIntegerProperty; 061import javafx.beans.property.ReadOnlyIntegerWrapper; 062import javafx.beans.property.SimpleBooleanProperty; 063import javafx.beans.property.ReadOnlyObjectProperty; 064import javafx.beans.property.ReadOnlyObjectWrapper; 065import javafx.beans.value.WeakChangeListener; 066 067/** 068 * The TreeView control provides a view on to a tree root (of type 069 * {@link TreeItem}). By using a TreeView, it is possible to drill down into the 070 * children of a TreeItem, recursively until a TreeItem has no children (that is, 071 * it is a <i>leaf</i> node in the tree). To facilitate this, unlike controls 072 * like {@link ListView}, in TreeView it is necessary to <strong>only</strong> 073 * specify the {@link #rootProperty() root} node. 074 * 075 * <p> 076 * For more information on building up a tree using this approach, refer to the 077 * {@link TreeItem} class documentation. Briefly however, to create a TreeView, 078 * you should do something along the lines of the following: 079 * <pre><code> 080 * TreeItem<String> root = new TreeItem<String>("Root Node"); 081 * root.setExpanded(true); 082 * root.getChildren().addAll( 083 * new TreeItem<String>("Item 1"), 084 * new TreeItem<String>("Item 2"), 085 * new TreeItem<String>("Item 3") 086 * ); 087 * TreeView<String> treeView = new TreeView<String>(root); 088 * </code></pre> 089 * 090 * <p> 091 * A TreeView may be configured to optionally hide the root node by setting the 092 * {@link #setShowRoot(boolean) showRoot} property to {@code false}. If the root 093 * node is hidden, there is one less level of indentation, and all children 094 * nodes of the root node are shown. By default, the root node is shown in the 095 * TreeView. 096 * 097 * <h3>TreeView Selection / Focus APIs</h3> 098 * <p>To track selection and focus, it is necessary to become familiar with the 099 * {@link SelectionModel} and {@link FocusModel} classes. A TreeView has at most 100 * one instance of each of these classes, available from 101 * {@link #selectionModelProperty() selectionModel} and 102 * {@link #focusModelProperty() focusModel} properties respectively. 103 * Whilst it is possible to use this API to set a new selection model, in 104 * most circumstances this is not necessary - the default selection and focus 105 * models should work in most circumstances. 106 * 107 * <p>The default {@link SelectionModel} used when instantiating a TreeView is 108 * an implementation of the {@link MultipleSelectionModel} abstract class. 109 * However, as noted in the API documentation for 110 * the {@link MultipleSelectionModel#selectionModeProperty() selectionMode} 111 * property, the default value is {@link SelectionMode#SINGLE}. To enable 112 * multiple selection in a default TreeView instance, it is therefore necessary 113 * to do the following: 114 * 115 * <pre> 116 * {@code 117 * treeView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);}</pre> 118 * 119 * <h3>Customizing TreeView Visuals</h3> 120 * <p>The visuals of the TreeView can be entirely customized by replacing the 121 * default {@link #cellFactoryProperty() cell factory}. A cell factory is used to 122 * generate {@link TreeCell} instances, which are used to represent an item in the 123 * TreeView. See the {@link Cell} class documentation for a more complete 124 * description of how to write custom Cells. 125 * 126 * @see TreeItem 127 * @see TreeCell 128 * @param <T> The type of the item contained within the {@link TreeItem} value 129 * property for all tree items in this TreeView. 130 */ 131@DefaultProperty("root") 132public class TreeView<T> extends Control { 133 134 /*************************************************************************** 135 * * 136 * Static properties and methods * 137 * * 138 **************************************************************************/ 139 140 /** 141 * An EventType that indicates some edit event has occurred. It is the parent 142 * type of all other edit events: {@link #editStartEvent}, 143 * {@link #editCommitEvent} and {@link #editCancelEvent}. 144 * 145 * @return An EventType that indicates some edit event has occurred. 146 */ 147 @SuppressWarnings("unchecked") 148 public static <T> EventType<EditEvent<T>> editAnyEvent() { 149 return (EventType<EditEvent<T>>) EDIT_ANY_EVENT; 150 } 151 private static final EventType<?> EDIT_ANY_EVENT = 152 new EventType(Event.ANY, "TREE_VIEW_EDIT"); 153 154 /** 155 * An EventType used to indicate that an edit event has started within the 156 * TreeView upon which the event was fired. 157 * 158 * @return An EventType used to indicate that an edit event has started. 159 */ 160 @SuppressWarnings("unchecked") 161 public static <T> EventType<EditEvent<T>> editStartEvent() { 162 return (EventType<EditEvent<T>>) EDIT_START_EVENT; 163 } 164 private static final EventType<?> EDIT_START_EVENT = 165 new EventType(editAnyEvent(), "EDIT_START"); 166 167 /** 168 * An EventType used to indicate that an edit event has just been canceled 169 * within the TreeView upon which the event was fired. 170 * 171 * @return An EventType used to indicate that an edit event has just been 172 * canceled. 173 */ 174 @SuppressWarnings("unchecked") 175 public static <T> EventType<EditEvent<T>> editCancelEvent() { 176 return (EventType<EditEvent<T>>) EDIT_CANCEL_EVENT; 177 } 178 private static final EventType<?> EDIT_CANCEL_EVENT = 179 new EventType(editAnyEvent(), "EDIT_CANCEL"); 180 181 /** 182 * An EventType that is used to indicate that an edit in a TreeView has been 183 * committed. This means that user has made changes to the data of a 184 * TreeItem, and that the UI should be updated. 185 * 186 * @return An EventType that is used to indicate that an edit in a TreeView 187 * has been committed. 188 */ 189 @SuppressWarnings("unchecked") 190 public static <T> EventType<EditEvent<T>> editCommitEvent() { 191 return (EventType<EditEvent<T>>) EDIT_COMMIT_EVENT; 192 } 193 private static final EventType<?> EDIT_COMMIT_EVENT = 194 new EventType(editAnyEvent(), "EDIT_COMMIT"); 195 196 /** 197 * Returns the number of levels of 'indentation' of the given TreeItem, 198 * based on how many times getParent() can be recursively called. If the 199 * given TreeItem is the root node, or if the TreeItem does not have any 200 * parent set, the returned value will be zero. For each time getParent() is 201 * recursively called, the returned value is incremented by one. 202 * 203 * @param node The TreeItem for which the level is needed. 204 * @return An integer representing the number of parents above the given node, 205 * or -1 if the given TreeItem is null. 206 */ 207 public static int getNodeLevel(TreeItem<?> node) { 208 if (node == null) return -1; 209 210 int level = 0; 211 TreeItem parent = node.getParent(); 212 while (parent != null) { 213 level++; 214 parent = parent.getParent(); 215 } 216 217 return level; 218 } 219 220 221 /*************************************************************************** 222 * * 223 * Constructors * 224 * * 225 **************************************************************************/ 226 227 /** 228 * Creates an empty TreeView. 229 * 230 * <p>Refer to the {@link TreeView} class documentation for details on the 231 * default state of other properties. 232 */ 233 public TreeView() { 234 this(null); 235 } 236 237 /** 238 * Creates a TreeView with the provided root node. 239 * 240 * <p>Refer to the {@link TreeView} class documentation for details on the 241 * default state of other properties. 242 * 243 * @param root The node to be the root in this TreeView. 244 */ 245 public TreeView(TreeItem<T> root) { 246 getStyleClass().setAll("tree-view"); 247 248 setRoot(root); 249 updateExpandedItemCount(root); 250 251 // install default selection and focus models - it's unlikely this will be changed 252 // by many users. 253 MultipleSelectionModel sm = new TreeViewBitSetSelectionModel<T>(this); 254 setSelectionModel(sm); 255 setFocusModel(new TreeViewFocusModel<T>(this)); 256 } 257 258 259 260 /*************************************************************************** 261 * * 262 * Instance Variables * 263 * * 264 **************************************************************************/ 265 266 // used in the tree item modification event listener. Used by the 267 // layoutChildren method to determine whether the tree item count should 268 // be recalculated. 269 private boolean expandedItemCountDirty = true; 270 271 272 /*************************************************************************** 273 * * 274 * Callbacks and Events * 275 * * 276 **************************************************************************/ 277 278 // we use this to forward events that have bubbled up TreeItem instances 279 // to the TreeViewSkin, to force it to recalculate teh item count and redraw 280 // if necessary 281 private final EventHandler<TreeModificationEvent<T>> rootEvent = new EventHandler<TreeModificationEvent<T>>() { 282 @Override public void handle(TreeModificationEvent<T> e) { 283 // this forces layoutChildren at the next pulse, and therefore 284 // updates the item count if necessary 285 EventType eventType = e.getEventType(); 286 boolean match = false; 287 while (eventType != null) { 288 if (eventType.equals(TreeItem.<T>expandedItemCountChangeEvent())) { 289 match = true; 290 break; 291 } 292 eventType = eventType.getSuperType(); 293 } 294 295 if (match) { 296 expandedItemCountDirty = true; 297 requestLayout(); 298 } 299 } 300 }; 301 302 private WeakEventHandler weakRootEventListener; 303 304 305 /*************************************************************************** 306 * * 307 * Properties * 308 * * 309 **************************************************************************/ 310 311 312 // --- Cell Factory 313 private ObjectProperty<Callback<TreeView<T>, TreeCell<T>>> cellFactory; 314 315 /** 316 * Sets the cell factory that will be used for creating TreeCells, 317 * which are used to represent items in the 318 * TreeView. The factory works identically to the cellFactory in ListView 319 * and other complex composite controls. It is called to create a new 320 * TreeCell only when the system has determined that it doesn't have enough 321 * cells to represent the currently visible items. The TreeCell is reused 322 * by the system to represent different items in the tree when possible. 323 * 324 * <p>Refer to the {@link Cell} class documentation for more details. 325 * 326 * @param value The {@link Callback} to use for generating TreeCell instances, 327 * or null if the default cell factory should be used. 328 */ 329 public final void setCellFactory(Callback<TreeView<T>, TreeCell<T>> value) { 330 cellFactoryProperty().set(value); 331 } 332 333 /** 334 * <p>Returns the cell factory that will be used for creating TreeCells, 335 * which are used to represent items in the TreeView, or null if no custom 336 * cell factory has been set. 337 */ 338 public final Callback<TreeView<T>, TreeCell<T>> getCellFactory() { 339 return cellFactory == null ? null : cellFactory.get(); 340 } 341 342 /** 343 * Represents the cell factory that will be used for creating TreeCells, 344 * which are used to represent items in the TreeView. 345 */ 346 public final ObjectProperty<Callback<TreeView<T>, TreeCell<T>>> cellFactoryProperty() { 347 if (cellFactory == null) { 348 cellFactory = new SimpleObjectProperty<Callback<TreeView<T>, TreeCell<T>>>(this, "cellFactory"); 349 } 350 return cellFactory; 351 } 352 353 354 // --- Root 355 private ObjectProperty<TreeItem<T>> root = new SimpleObjectProperty<TreeItem<T>>(this, "root") { 356 private WeakReference<TreeItem<T>> weakOldItem; 357 358 @Override protected void invalidated() { 359 TreeItem<T> oldTreeItem = weakOldItem == null ? null : weakOldItem.get(); 360 if (oldTreeItem != null && weakRootEventListener != null) { 361 oldTreeItem.removeEventHandler(TreeItem.<T>treeNotificationEvent(), weakRootEventListener); 362 } 363 364 TreeItem<T> root = getRoot(); 365 if (root != null) { 366 weakRootEventListener = new WeakEventHandler(rootEvent); 367 getRoot().addEventHandler(TreeItem.<T>treeNotificationEvent(), weakRootEventListener); 368 weakOldItem = new WeakReference<TreeItem<T>>(root); 369 } 370 371 expandedItemCountDirty = true; 372 updateRootExpanded(); 373 } 374 }; 375 376 /** 377 * Sets the root node in this TreeView. See the {@link TreeItem} class level 378 * documentation for more details. 379 * 380 * @param value The {@link TreeItem} that will be placed at the root of the 381 * TreeView. 382 */ 383 public final void setRoot(TreeItem<T> value) { 384 rootProperty().set(value); 385 } 386 387 /** 388 * Returns the current root node of this TreeView, or null if no root node 389 * is specified. 390 * @return The current root node, or null if no root node exists. 391 */ 392 public final TreeItem<T> getRoot() { 393 return root == null ? null : root.get(); 394 } 395 396 /** 397 * Property representing the root node of the TreeView. 398 */ 399 public final ObjectProperty<TreeItem<T>> rootProperty() { 400 return root; 401 } 402 403 404 405 // --- Show Root 406 private BooleanProperty showRoot; 407 408 /** 409 * Specifies whether the root {@code TreeItem} should be shown within this 410 * TreeView. 411 * 412 * @param value If true, the root TreeItem will be shown, and if false it 413 * will be hidden. 414 */ 415 public final void setShowRoot(boolean value) { 416 showRootProperty().set(value); 417 } 418 419 /** 420 * Returns true if the root of the TreeView should be shown, and false if 421 * it should not. By default, the root TreeItem is visible in the TreeView. 422 */ 423 public final boolean isShowRoot() { 424 return showRoot == null ? true : showRoot.get(); 425 } 426 427 /** 428 * Property that represents whether or not the TreeView root node is visible. 429 */ 430 public final BooleanProperty showRootProperty() { 431 if (showRoot == null) { 432 showRoot = new SimpleBooleanProperty(this, "showRoot", true) { 433 @Override protected void invalidated() { 434 updateRootExpanded(); 435 updateExpandedItemCount(getRoot()); 436 } 437 }; 438 } 439 return showRoot; 440 } 441 442 443 // --- Selection Model 444 private ObjectProperty<MultipleSelectionModel<TreeItem<T>>> selectionModel; 445 446 /** 447 * Sets the {@link MultipleSelectionModel} to be used in the TreeView. 448 * Despite a TreeView requiring a <code><b>Multiple</b>SelectionModel</code>, 449 * it is possible to configure it to only allow single selection (see 450 * {@link MultipleSelectionModel#setSelectionMode(javafx.scene.control.SelectionMode)} 451 * for more information). 452 */ 453 public final void setSelectionModel(MultipleSelectionModel<TreeItem<T>> value) { 454 selectionModelProperty().set(value); 455 } 456 457 /** 458 * Returns the currently installed selection model. 459 */ 460 public final MultipleSelectionModel<TreeItem<T>> getSelectionModel() { 461 return selectionModel == null ? null : selectionModel.get(); 462 } 463 464 /** 465 * The SelectionModel provides the API through which it is possible 466 * to select single or multiple items within a TreeView, as well as inspect 467 * which rows have been selected by the user. Note that it has a generic 468 * type that must match the type of the TreeView itself. 469 */ 470 public final ObjectProperty<MultipleSelectionModel<TreeItem<T>>> selectionModelProperty() { 471 if (selectionModel == null) { 472 selectionModel = new SimpleObjectProperty<MultipleSelectionModel<TreeItem<T>>>(this, "selectionModel"); 473 } 474 return selectionModel; 475 } 476 477 478 // --- Focus Model 479 private ObjectProperty<FocusModel<TreeItem<T>>> focusModel; 480 481 /** 482 * Sets the {@link FocusModel} to be used in the TreeView. 483 */ 484 public final void setFocusModel(FocusModel<TreeItem<T>> value) { 485 focusModelProperty().set(value); 486 } 487 488 /** 489 * Returns the currently installed {@link FocusModel}. 490 */ 491 public final FocusModel<TreeItem<T>> getFocusModel() { 492 return focusModel == null ? null : focusModel.get(); 493 } 494 495 /** 496 * The FocusModel provides the API through which it is possible 497 * to control focus on zero or one rows of the TreeView. Generally the 498 * default implementation should be more than sufficient. 499 */ 500 public final ObjectProperty<FocusModel<TreeItem<T>>> focusModelProperty() { 501 if (focusModel == null) { 502 focusModel = new SimpleObjectProperty<FocusModel<TreeItem<T>>>(this, "focusModel"); 503 } 504 return focusModel; 505 } 506 507 508 // --- Expanded node count 509 /** 510 * <p>Represents the number of tree nodes presently able to be visible in the 511 * TreeView. This is essentially the count of all expanded tree items, and 512 * their children. 513 * 514 * <p>For example, if just the root node is visible, the expandedItemCount will 515 * be one. If the root had three children and the root was expanded, the value 516 * will be four. 517 */ 518 private ReadOnlyIntegerWrapper expandedItemCount = new ReadOnlyIntegerWrapper(this, "expandedItemCount", 0); 519 public final ReadOnlyIntegerProperty expandedItemCountProperty() { 520 return expandedItemCount.getReadOnlyProperty(); 521 } 522 private void setExpandedItemCount(int value) { 523 expandedItemCount.set(value); 524 } 525 public final int getExpandedItemCount() { 526 if (expandedItemCountDirty) { 527 updateExpandedItemCount(getRoot()); 528 } 529 return expandedItemCount.get(); 530 } 531 532 533 // --- Fixed cell size 534 private DoubleProperty fixedCellSize; 535 536 /** 537 * Sets the new fixed cell size for this control. Any value greater than 538 * zero will enable fixed cell size mode, whereas a zero or negative value 539 * (or Region.USE_COMPUTED_SIZE) will be used to disabled fixed cell size 540 * mode. 541 * 542 * @param value The new fixed cell size value, or -1 (or Region.USE_COMPUTED_SIZE) 543 * to disable. 544 */ 545 public final void setFixedCellSize(double value) { 546 fixedCellSizeProperty().set(value); 547 } 548 549 /** 550 * Returns the fixed cell size value, which may be -1 to represent fixed cell 551 * size mode is disabled, or a value greater than zero to represent the size 552 * of all cells in this control. 553 * 554 * @return A double representing the fixed cell size of this control, or -1 555 * if fixed cell size mode is disabled. 556 */ 557 public final double getFixedCellSize() { 558 return fixedCellSize == null ? Region.USE_COMPUTED_SIZE : fixedCellSize.get(); 559 } 560 /** 561 * Specifies whether this control has cells that are a fixed height (of the 562 * specified value). If this value is -1 (i.e. {@link Region#USE_COMPUTED_SIZE}), 563 * then all cells are individually sized and positioned. This is a slow 564 * operation. Therefore, when performance matters and developers are not 565 * dependent on variable cell sizes it is a good idea to set the fixed cell 566 * size value. Generally cells are around 24px, so setting a fixed cell size 567 * of 24 is likely to result in very little difference in visuals, but a 568 * improvement to performance. 569 * 570 * <p>To set this property via CSS, use the -fx-fixed-cell-size property. 571 * This should not be confused with the -fx-cell-size property. The difference 572 * between these two CSS properties is that -fx-cell-size will size all 573 * cells to the specified size, but it will not enforce that this is the 574 * only size (thus allowing for variable cell sizes, and preventing the 575 * performance gains from being possible). Therefore, when performance matters 576 * use -fx-fixed-cell-size, instead of -fx-cell-size. If both properties are 577 * specified in CSS, -fx-fixed-cell-size takes precedence.</p> 578 */ 579 public final DoubleProperty fixedCellSizeProperty() { 580 if (fixedCellSize == null) { 581 fixedCellSize = new StyleableDoubleProperty(Region.USE_COMPUTED_SIZE) { 582 @Override public CssMetaData<TreeView<?>,Number> getCssMetaData() { 583 return StyleableProperties.FIXED_CELL_SIZE; 584 } 585 586 @Override public Object getBean() { 587 return TreeView.this; 588 } 589 590 @Override public String getName() { 591 return "fixedCellSize"; 592 } 593 }; 594 } 595 return fixedCellSize; 596 } 597 598 599 // --- Editable 600 private BooleanProperty editable; 601 public final void setEditable(boolean value) { 602 editableProperty().set(value); 603 } 604 public final boolean isEditable() { 605 return editable == null ? false : editable.get(); 606 } 607 /** 608 * Specifies whether this TreeView is editable - only if the TreeView and 609 * the TreeCells within it are both editable will a TreeCell be able to go 610 * into their editing state. 611 */ 612 public final BooleanProperty editableProperty() { 613 if (editable == null) { 614 editable = new SimpleBooleanProperty(this, "editable", false); 615 } 616 return editable; 617 } 618 619 620 // --- Editing Item 621 private ReadOnlyObjectWrapper<TreeItem<T>> editingItem; 622 623 private void setEditingItem(TreeItem<T> value) { 624 editingItemPropertyImpl().set(value); 625 } 626 627 /** 628 * Returns the TreeItem that is currently being edited in the TreeView, 629 * or null if no item is being edited. 630 */ 631 public final TreeItem<T> getEditingItem() { 632 return editingItem == null ? null : editingItem.get(); 633 } 634 635 /** 636 * <p>A property used to represent the TreeItem currently being edited 637 * in the TreeView, if editing is taking place, or -1 if no item is being edited. 638 * 639 * <p>It is not possible to set the editing item, instead it is required that 640 * you call {@link #edit(javafx.scene.control.TreeItem)}. 641 */ 642 public final ReadOnlyObjectProperty<TreeItem<T>> editingItemProperty() { 643 return editingItemPropertyImpl().getReadOnlyProperty(); 644 } 645 646 private ReadOnlyObjectWrapper<TreeItem<T>> editingItemPropertyImpl() { 647 if (editingItem == null) { 648 editingItem = new ReadOnlyObjectWrapper<TreeItem<T>>(this, "editingItem"); 649 } 650 return editingItem; 651 } 652 653 654 // --- On Edit Start 655 private ObjectProperty<EventHandler<EditEvent<T>>> onEditStart; 656 657 /** 658 * Sets the {@link EventHandler} that will be called when the user begins 659 * an edit. 660 */ 661 public final void setOnEditStart(EventHandler<EditEvent<T>> value) { 662 onEditStartProperty().set(value); 663 } 664 665 /** 666 * Returns the {@link EventHandler} that will be called when the user begins 667 * an edit. 668 */ 669 public final EventHandler<EditEvent<T>> getOnEditStart() { 670 return onEditStart == null ? null : onEditStart.get(); 671 } 672 673 /** 674 * This event handler will be fired when the user successfully initiates 675 * editing. 676 */ 677 public final ObjectProperty<EventHandler<EditEvent<T>>> onEditStartProperty() { 678 if (onEditStart == null) { 679 onEditStart = new SimpleObjectProperty<EventHandler<EditEvent<T>>>(this, "onEditStart") { 680 @Override protected void invalidated() { 681 setEventHandler(TreeView.<T>editStartEvent(), get()); 682 } 683 }; 684 } 685 return onEditStart; 686 } 687 688 689 // --- On Edit Commit 690 private ObjectProperty<EventHandler<EditEvent<T>>> onEditCommit; 691 692 /** 693 * Sets the {@link EventHandler} that will be called when the user commits 694 * an edit. 695 */ 696 public final void setOnEditCommit(EventHandler<EditEvent<T>> value) { 697 onEditCommitProperty().set(value); 698 } 699 700 /** 701 * Returns the {@link EventHandler} that will be called when the user commits 702 * an edit. 703 */ 704 public final EventHandler<EditEvent<T>> getOnEditCommit() { 705 return onEditCommit == null ? null : onEditCommit.get(); 706 } 707 708 /** 709 * <p>This property is used when the user performs an action that should 710 * result in their editing input being persisted.</p> 711 * 712 * <p>The EventHandler in this property should not be called directly - 713 * instead call {@link TreeCell#commitEdit(java.lang.Object)} from within 714 * your custom TreeCell. This will handle firing this event, updating the 715 * view, and switching out of the editing state.</p> 716 */ 717 public final ObjectProperty<EventHandler<EditEvent<T>>> onEditCommitProperty() { 718 if (onEditCommit == null) { 719 onEditCommit = new SimpleObjectProperty<EventHandler<EditEvent<T>>>(this, "onEditCommit") { 720 @Override protected void invalidated() { 721 setEventHandler(TreeView.<T>editCommitEvent(), get()); 722 } 723 }; 724 } 725 return onEditCommit; 726 } 727 728 729 // --- On Edit Cancel 730 private ObjectProperty<EventHandler<EditEvent<T>>> onEditCancel; 731 732 /** 733 * Sets the {@link EventHandler} that will be called when the user cancels 734 * an edit. 735 */ 736 public final void setOnEditCancel(EventHandler<EditEvent<T>> value) { 737 onEditCancelProperty().set(value); 738 } 739 740 /** 741 * Returns the {@link EventHandler} that will be called when the user cancels 742 * an edit. 743 */ 744 public final EventHandler<EditEvent<T>> getOnEditCancel() { 745 return onEditCancel == null ? null : onEditCancel.get(); 746 } 747 748 /** 749 * This event handler will be fired when the user cancels editing a cell. 750 */ 751 public final ObjectProperty<EventHandler<EditEvent<T>>> onEditCancelProperty() { 752 if (onEditCancel == null) { 753 onEditCancel = new SimpleObjectProperty<EventHandler<EditEvent<T>>>(this, "onEditCancel") { 754 @Override protected void invalidated() { 755 setEventHandler(TreeView.<T>editCancelEvent(), get()); 756 } 757 }; 758 } 759 return onEditCancel; 760 } 761 762 763 764 /*************************************************************************** 765 * * 766 * Public API * 767 * * 768 **************************************************************************/ 769 770 771 /** {@inheritDoc} */ 772 @Override protected void layoutChildren() { 773 if (expandedItemCountDirty) { 774 updateExpandedItemCount(getRoot()); 775 } 776 777 super.layoutChildren(); 778 } 779 780 781 /** 782 * Instructs the TreeView to begin editing the given TreeItem, if 783 * the TreeView is {@link #editableProperty() editable}. Once 784 * this method is called, if the current 785 * {@link #cellFactoryProperty() cell factory} is set up to support editing, 786 * the Cell will switch its visual state to enable the user input to take place. 787 * 788 * @param item The TreeItem in the TreeView that should be edited. 789 */ 790 public void edit(TreeItem<T> item) { 791 if (!isEditable()) return; 792 setEditingItem(item); 793 } 794 795 796 /** 797 * Scrolls the TreeView such that the item in the given index is visible to 798 * the end user. 799 * 800 * @param index The index that should be made visible to the user, assuming 801 * of course that it is greater than, or equal to 0, and less than the 802 * number of the visible items in the TreeView. 803 */ 804 public void scrollTo(int index) { 805 ControlUtils.scrollToIndex(this, index); 806 } 807 808 /** 809 * Called when there's a request to scroll an index into view using {@link #scrollTo(int)} 810 */ 811 private ObjectProperty<EventHandler<ScrollToEvent<Integer>>> onScrollTo; 812 813 public void setOnScrollTo(EventHandler<ScrollToEvent<Integer>> value) { 814 onScrollToProperty().set(value); 815 } 816 817 public EventHandler<ScrollToEvent<Integer>> getOnScrollTo() { 818 if( onScrollTo != null ) { 819 return onScrollTo.get(); 820 } 821 return null; 822 } 823 824 public ObjectProperty<EventHandler<ScrollToEvent<Integer>>> onScrollToProperty() { 825 if( onScrollTo == null ) { 826 onScrollTo = new ObjectPropertyBase<EventHandler<ScrollToEvent<Integer>>>() { 827 @Override 828 protected void invalidated() { 829 setEventHandler(ScrollToEvent.scrollToTopIndex(), get()); 830 } 831 @Override 832 public Object getBean() { 833 return TreeView.this; 834 } 835 836 @Override 837 public String getName() { 838 return "onScrollTo"; 839 } 840 }; 841 } 842 return onScrollTo; 843 } 844 845 /** 846 * Returns the index position of the given TreeItem, taking into account the 847 * current state of each TreeItem (i.e. whether or not it is expanded). 848 * 849 * @param item The TreeItem for which the index is sought. 850 * @return An integer representing the location in the current TreeView of the 851 * first instance of the given TreeItem, or -1 if it is null or can not 852 * be found. 853 */ 854 public int getRow(TreeItem<T> item) { 855 return TreeUtil.getRow(item, getRoot(), expandedItemCountDirty, isShowRoot()); 856 } 857 858 /** 859 * Returns the TreeItem in the given index, or null if it is out of bounds. 860 * 861 * @param row The index of the TreeItem being sought. 862 * @return The TreeItem in the given index, or null if it is out of bounds. 863 */ 864 public TreeItem<T> getTreeItem(int row) { 865 // normalize the requested row based on whether showRoot is set 866 int r = isShowRoot() ? row : (row + 1); 867 return TreeUtil.getItem(getRoot(), r, expandedItemCountDirty); 868 } 869 870 /** {@inheritDoc} */ 871 @Override protected Skin<?> createDefaultSkin() { 872 return new TreeViewSkin(this); 873 } 874 875 /*************************************************************************** 876 * * 877 * Private Implementation * 878 * * 879 **************************************************************************/ 880 881 private void updateExpandedItemCount(TreeItem treeItem) { 882 setExpandedItemCount(TreeUtil.updateExpandedItemCount(treeItem, expandedItemCountDirty, isShowRoot())); 883 expandedItemCountDirty = false; 884 } 885 886 private void updateRootExpanded() { 887 // if we aren't showing the root, and the root isn't expanded, we expand 888 // it now so that something is shown. 889 if (!isShowRoot() && getRoot() != null && ! getRoot().isExpanded()) { 890 getRoot().setExpanded(true); 891 } 892 } 893 894 895 896 /*************************************************************************** 897 * * 898 * Stylesheet Handling * 899 * * 900 **************************************************************************/ 901 902 private static final String DEFAULT_STYLE_CLASS = "table-view"; 903 904 /** @treatAsPrivate */ 905 private static class StyleableProperties { 906 private static final CssMetaData<TreeView<?>,Number> FIXED_CELL_SIZE = 907 new CssMetaData<TreeView<?>,Number>("-fx-fixed-cell-size", 908 SizeConverter.getInstance(), 909 Region.USE_COMPUTED_SIZE) { 910 911 @Override public Double getInitialValue(TreeView node) { 912 return node.getFixedCellSize(); 913 } 914 915 @Override public boolean isSettable(TreeView n) { 916 return n.fixedCellSize == null || !n.fixedCellSize.isBound(); 917 } 918 919 @Override public StyleableProperty<Number> getStyleableProperty(TreeView n) { 920 return (StyleableProperty<Number>) n.fixedCellSizeProperty(); 921 } 922 }; 923 924 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 925 static { 926 final List<CssMetaData<? extends Styleable, ?>> styleables = 927 new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData()); 928 styleables.add(FIXED_CELL_SIZE); 929 STYLEABLES = Collections.unmodifiableList(styleables); 930 } 931 } 932 933 /** 934 * @return The CssMetaData associated with this class, which may include the 935 * CssMetaData of its super classes. 936 */ 937 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 938 return StyleableProperties.STYLEABLES; 939 } 940 941 /** 942 * {@inheritDoc} 943 */ 944 @Override 945 public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() { 946 return getClassCssMetaData(); 947 } 948 949 950 951 /*************************************************************************** 952 * * 953 * Support Interfaces * 954 * * 955 **************************************************************************/ 956 957 958 959 /*************************************************************************** 960 * * 961 * Support Classes * 962 * * 963 **************************************************************************/ 964 965 966 /** 967 * An {@link Event} subclass used specifically in TreeView for representing 968 * edit-related events. It provides additional API to easily access the 969 * TreeItem that the edit event took place on, as well as the input provided 970 * by the end user. 971 * 972 * @param <T> The type of the input, which is the same type as the TreeView 973 * itself. 974 */ 975 public static class EditEvent<T> extends Event { 976 private static final long serialVersionUID = -4437033058917528976L; 977 978 /** 979 * Common supertype for all edit event types. 980 */ 981 public static final EventType<?> ANY = EDIT_ANY_EVENT; 982 983 private final T oldValue; 984 private final T newValue; 985 private transient final TreeItem<T> treeItem; 986 987 /** 988 * Creates a new EditEvent instance to represent an edit event. This 989 * event is used for {@link #EDIT_START_EVENT}, 990 * {@link #EDIT_COMMIT_EVENT} and {@link #EDIT_CANCEL_EVENT} types. 991 */ 992 public EditEvent(TreeView<T> source, 993 EventType<? extends EditEvent> eventType, 994 TreeItem<T> treeItem, T oldValue, T newValue) { 995 super(source, Event.NULL_SOURCE_TARGET, eventType); 996 this.oldValue = oldValue; 997 this.newValue = newValue; 998 this.treeItem = treeItem; 999 } 1000 1001 /** 1002 * Returns the TreeView upon which the edit took place. 1003 */ 1004 @Override public TreeView<T> getSource() { 1005 return (TreeView) super.getSource(); 1006 } 1007 1008 /** 1009 * Returns the {@link TreeItem} upon which the edit took place. 1010 */ 1011 public TreeItem<T> getTreeItem() { 1012 return treeItem; 1013 } 1014 1015 /** 1016 * Returns the new value input into the TreeItem by the end user. 1017 */ 1018 public T getNewValue() { 1019 return newValue; 1020 } 1021 1022 /** 1023 * Returns the old value that existed in the TreeItem prior to the current 1024 * edit event. 1025 */ 1026 public T getOldValue() { 1027 return oldValue; 1028 } 1029 } 1030 1031 1032 1033 1034 1035 1036 1037 // package for testing 1038 static class TreeViewBitSetSelectionModel<T> extends MultipleSelectionModelBase<TreeItem<T>> { 1039 1040 /*********************************************************************** 1041 * * 1042 * Constructors * 1043 * * 1044 **********************************************************************/ 1045 1046 public TreeViewBitSetSelectionModel(final TreeView<T> treeView) { 1047 if (treeView == null) { 1048 throw new IllegalArgumentException("TreeView can not be null"); 1049 } 1050 1051 this.treeView = treeView; 1052 this.treeView.rootProperty().addListener(weakRootPropertyListener); 1053 1054 updateTreeEventListener(null, treeView.getRoot()); 1055 } 1056 1057 private void updateTreeEventListener(TreeItem<T> oldRoot, TreeItem<T> newRoot) { 1058 if (oldRoot != null && weakTreeItemListener != null) { 1059 oldRoot.removeEventHandler(TreeItem.<T>expandedItemCountChangeEvent(), weakTreeItemListener); 1060 } 1061 1062 if (newRoot != null) { 1063 weakTreeItemListener = new WeakEventHandler(treeItemListener); 1064 newRoot.addEventHandler(TreeItem.<T>expandedItemCountChangeEvent(), weakTreeItemListener); 1065 } 1066 } 1067 1068 private ChangeListener rootPropertyListener = new ChangeListener<TreeItem<T>>() { 1069 @Override public void changed(ObservableValue<? extends TreeItem<T>> observable, 1070 TreeItem<T> oldValue, TreeItem<T> newValue) { 1071 clearSelection(); 1072 updateTreeEventListener(oldValue, newValue); 1073 } 1074 }; 1075 1076 private EventHandler<TreeModificationEvent<T>> treeItemListener = new EventHandler<TreeModificationEvent<T>>() { 1077 @Override public void handle(TreeModificationEvent<T> e) { 1078 1079 if (getSelectedIndex() == -1 && getSelectedItem() == null) return; 1080 1081 final TreeItem<T> treeItem = e.getTreeItem(); 1082 if (treeItem == null) return; 1083 1084 // we only shift selection from this row - everything before it 1085 // is safe. We might change this below based on certain criteria 1086 int startRow = treeView.getRow(treeItem); 1087 1088 int shift = 0; 1089 if (e.wasExpanded()) { 1090 // need to shuffle selection by the number of visible children 1091 shift = treeItem.getExpandedDescendentCount(false) - 1; 1092 startRow++; 1093 } else if (e.wasCollapsed()) { 1094 // remove selection from any child treeItem 1095 treeItem.getExpandedDescendentCount(false); 1096 int count = treeItem.previousExpandedDescendentCount; 1097 boolean wasAnyChildSelected = false; 1098 for (int i = startRow; i < startRow + count; i++) { 1099 if (isSelected(i)) { 1100 wasAnyChildSelected = true; 1101 break; 1102 } 1103 } 1104 1105 // put selection onto the newly-collapsed tree item 1106 if (wasAnyChildSelected) { 1107 select(startRow); 1108 } 1109 1110 shift = - count + 1; 1111 startRow++; 1112 } else if (e.wasAdded()) { 1113 // shuffle selection by the number of added items 1114 shift = treeItem.isExpanded() ? e.getAddedSize() : 0; 1115 } else if (e.wasRemoved()) { 1116 // shuffle selection by the number of removed items 1117 shift = treeItem.isExpanded() ? -e.getRemovedSize() : 0; 1118 1119 // whilst we are here, we should check if the removed items 1120 // are part of the selectedItems list - and remove them 1121 // from selection if they are (as per RT-15446) 1122 final List<Integer> selectedIndices = getSelectedIndices(); 1123 final int selectedIndex = getSelectedIndex(); 1124 final List<TreeItem<T>> selectedItems = getSelectedItems(); 1125 final TreeItem<T> selectedItem = getSelectedItem(); 1126 final List<? extends TreeItem<T>> removedChildren = e.getRemovedChildren(); 1127 1128 for (int i = 0; i < selectedIndices.size() && ! selectedItems.isEmpty(); i++) { 1129 int index = selectedIndices.get(i); 1130 if (index > selectedItems.size()) break; 1131 1132 TreeItem<T> item = selectedItems.get(index); 1133 if (item == null || removedChildren.contains(item)) { 1134 clearSelection(index); 1135 } else if (removedChildren.size() == 1 && 1136 selectedItems.size() == 1 && 1137 selectedItem != null && 1138 selectedItem.equals(removedChildren.get(0))) { 1139 // Bug fix for RT-28637 1140 if (selectedIndex < getItemCount()) { 1141 TreeItem<T> newSelectedItem = getModelItem(selectedIndex); 1142 if (! selectedItem.equals(newSelectedItem)) { 1143 setSelectedItem(newSelectedItem); 1144 } 1145 } 1146 } 1147 } 1148 } 1149 1150 treeView.expandedItemCountDirty = true; 1151 shiftSelection(startRow, shift, null); 1152 } 1153 }; 1154 1155 private WeakChangeListener weakRootPropertyListener = 1156 new WeakChangeListener(rootPropertyListener); 1157 1158 private WeakEventHandler weakTreeItemListener; 1159 1160 1161 /*********************************************************************** 1162 * * 1163 * Internal properties * 1164 * * 1165 **********************************************************************/ 1166 1167 private final TreeView<T> treeView; 1168 1169 1170 1171 /*********************************************************************** 1172 * * 1173 * Public selection API * 1174 * * 1175 **********************************************************************/ 1176 1177 /** {@inheritDoc} */ 1178 @Override public void select(TreeItem<T> obj) { 1179// if (getRowCount() <= 0) return; 1180 1181 if (obj == null && getSelectionMode() == SelectionMode.SINGLE) { 1182 clearSelection(); 1183 return; 1184 } 1185 1186 // we firstly expand the path down such that the given object is 1187 // visible. This fixes RT-14456, where selection was not happening 1188 // correctly on TreeItems that are not visible. 1189 TreeItem<?> item = obj; 1190 while (item != null) { 1191 item.setExpanded(true); 1192 item = item.getParent(); 1193 } 1194 1195 // Fix for RT-15419. We eagerly update the tree item count, such that 1196 // selection occurs on the row 1197 treeView.updateExpandedItemCount(treeView.getRoot()); 1198 1199 // We have no option but to iterate through the model and select the 1200 // first occurrence of the given object. Once we find the first one, we 1201 // don't proceed to select any others. 1202 int row = treeView.getRow(obj); 1203 1204 if (row == -1) { 1205 // if we are here, we did not find the item in the entire data model. 1206 // Even still, we allow for this item to be set to the give object. 1207 // We expect that in concrete subclasses of this class we observe the 1208 // data model such that we check to see if the given item exists in it, 1209 // whilst SelectedIndex == -1 && SelectedItem != null. 1210 setSelectedItem(obj); 1211 } else { 1212 select(row); 1213 } 1214 } 1215 1216 1217 1218 /*********************************************************************** 1219 * * 1220 * Support code * 1221 * * 1222 **********************************************************************/ 1223 1224 /** {@inheritDoc} */ 1225 @Override protected void focus(int itemIndex) { 1226 if (treeView.getFocusModel() != null) { 1227 treeView.getFocusModel().focus(itemIndex); 1228 } 1229 } 1230 1231 /** {@inheritDoc} */ 1232 @Override protected int getFocusedIndex() { 1233 if (treeView.getFocusModel() == null) return -1; 1234 return treeView.getFocusModel().getFocusedIndex(); 1235 } 1236 1237 /** {@inheritDoc} */ 1238 @Override protected int getItemCount() { 1239 return treeView == null ? 0 : treeView.getExpandedItemCount(); 1240 } 1241 1242 /** {@inheritDoc} */ 1243 @Override public TreeItem<T> getModelItem(int index) { 1244 if (treeView == null) return null; 1245 1246 if (index < 0 || index >= treeView.getExpandedItemCount()) return null; 1247 1248 return treeView.getTreeItem(index); 1249 } 1250 } 1251 1252 1253 1254 /** 1255 * 1256 * @param <T> 1257 */ 1258 static class TreeViewFocusModel<T> extends FocusModel<TreeItem<T>> { 1259 1260 private final TreeView<T> treeView; 1261 1262 public TreeViewFocusModel(final TreeView<T> treeView) { 1263 this.treeView = treeView; 1264 this.treeView.rootProperty().addListener(weakRootPropertyListener); 1265 updateTreeEventListener(null, treeView.getRoot()); 1266 } 1267 1268 private final ChangeListener rootPropertyListener = new ChangeListener<TreeItem<T>>() { 1269 @Override 1270 public void changed(ObservableValue<? extends TreeItem<T>> observable, TreeItem<T> oldValue, TreeItem<T> newValue) { 1271 updateTreeEventListener(oldValue, newValue); 1272 } 1273 }; 1274 1275 private final WeakChangeListener weakRootPropertyListener = 1276 new WeakChangeListener(rootPropertyListener); 1277 1278 private void updateTreeEventListener(TreeItem<T> oldRoot, TreeItem<T> newRoot) { 1279 if (oldRoot != null && weakTreeItemListener != null) { 1280 oldRoot.removeEventHandler(TreeItem.<T>expandedItemCountChangeEvent(), weakTreeItemListener); 1281 } 1282 1283 if (newRoot != null) { 1284 weakTreeItemListener = new WeakEventHandler(treeItemListener); 1285 newRoot.addEventHandler(TreeItem.<T>expandedItemCountChangeEvent(), weakTreeItemListener); 1286 } 1287 } 1288 1289 private EventHandler<TreeModificationEvent<T>> treeItemListener = new EventHandler<TreeModificationEvent<T>>() { 1290 @Override public void handle(TreeModificationEvent<T> e) { 1291 // don't shift focus if the event occurred on a tree item after 1292 // the focused row, or if there is no focus index at present 1293 if (getFocusedIndex() == -1) return; 1294 1295 int row = treeView.getRow(e.getTreeItem()); 1296 int shift = 0; 1297 if (e.wasExpanded()) { 1298 if (row < getFocusedIndex()) { 1299 // need to shuffle selection by the number of visible children 1300 shift = e.getTreeItem().getExpandedDescendentCount(false) - 1; 1301 } 1302 } else if (e.wasCollapsed()) { 1303 if (row < getFocusedIndex()) { 1304 // need to shuffle selection by the number of visible children 1305 // that were just hidden 1306 shift = - e.getTreeItem().previousExpandedDescendentCount + 1; 1307 } 1308 } else if (e.wasAdded()) { 1309 for (int i = 0; i < e.getAddedChildren().size(); i++) { 1310 TreeItem item = e.getAddedChildren().get(i); 1311 row = treeView.getRow(item); 1312 1313 if (item != null && row <= getFocusedIndex()) { 1314// shift = e.getTreeItem().isExpanded() ? e.getAddedSize() : 0; 1315 shift += item.getExpandedDescendentCount(false); 1316 } 1317 } 1318 } else if (e.wasRemoved()) { 1319 for (int i = 0; i < e.getRemovedChildren().size(); i++) { 1320 TreeItem item = e.getRemovedChildren().get(i); 1321 if (item != null && item.equals(getFocusedItem())) { 1322 focus(-1); 1323 return; 1324 } 1325 } 1326 1327 if (row <= getFocusedIndex()) { 1328 // shuffle selection by the number of removed items 1329 shift = e.getTreeItem().isExpanded() ? -e.getRemovedSize() : 0; 1330 } 1331 } 1332 1333 if(shift != 0) { 1334 final int newFocus = getFocusedIndex() + shift; 1335 Platform.runLater(new Runnable() { 1336 @Override public void run() { 1337 focus(newFocus); 1338 } 1339 }); 1340 } 1341 } 1342 }; 1343 1344 private WeakEventHandler weakTreeItemListener; 1345 1346 @Override protected int getItemCount() { 1347 return treeView == null ? -1 : treeView.getExpandedItemCount(); 1348 } 1349 1350 @Override protected TreeItem<T> getModelItem(int index) { 1351 if (treeView == null) return null; 1352 1353 if (index < 0 || index >= treeView.getExpandedItemCount()) return null; 1354 1355 return treeView.getTreeItem(index); 1356 } 1357 1358 /** {@inheritDoc} */ 1359 @Override public void focus(int index) { 1360 if (treeView.expandedItemCountDirty) { 1361 treeView.updateExpandedItemCount(treeView.getRoot()); 1362 } 1363 1364 super.focus(index); 1365 } 1366 1367 1368 } 1369}