javafx.scene.control.TreeView Maven / Gradle / Ivy
/*
* Copyright (c) 2008, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene.control;
import javafx.css.converter.SizeConverter;
import com.sun.javafx.scene.control.Properties;
import com.sun.javafx.scene.control.behavior.TreeCellBehavior;
import javafx.scene.control.skin.TreeViewSkin;
import javafx.application.Platform;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.ListChangeListener;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableDoubleProperty;
import javafx.css.StyleableProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.event.WeakEventHandler;
import javafx.scene.AccessibleAttribute;
import javafx.scene.AccessibleRole;
import javafx.scene.control.TreeItem.TreeModificationEvent;
import javafx.scene.layout.Region;
import javafx.util.Callback;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* The TreeView control provides a view on to a tree root (of type
* {@link TreeItem}). By using a TreeView, it is possible to drill down into the
* children of a TreeItem, recursively until a TreeItem has no children (that is,
* it is a leaf node in the tree). To facilitate this, unlike controls
* like {@link ListView}, in TreeView it is necessary to only
* specify the {@link #rootProperty() root} node.
*
*
* For more information on building up a tree using this approach, refer to the
* {@link TreeItem} class documentation. Briefly however, to create a TreeView,
* you should do something along the lines of the following:
*
*
TreeItem<String> root = new TreeItem<>("Root Node");
* root.setExpanded(true);
* root.getChildren().addAll(
* new TreeItem<>("Item 1"),
* new TreeItem<>("Item 2"),
* new TreeItem<>("Item 3")
* );
* TreeView<String> treeView = new TreeView<>(root);
*
*
*
*
* A TreeView may be configured to optionally hide the root node by setting the
* {@link #setShowRoot(boolean) showRoot} property to {@code false}. If the root
* node is hidden, there is one less level of indentation, and all children
* nodes of the root node are shown. By default, the root node is shown in the
* TreeView.
*
*
TreeView Selection / Focus APIs
* To track selection and focus, it is necessary to become familiar with the
* {@link SelectionModel} and {@link FocusModel} classes. A TreeView has at most
* one instance of each of these classes, available from
* {@link #selectionModelProperty() selectionModel} and
* {@link #focusModelProperty() focusModel} properties respectively.
* Whilst it is possible to use this API to set a new selection model, in
* most circumstances this is not necessary - the default selection and focus
* models should work in most circumstances.
*
*
The default {@link SelectionModel} used when instantiating a TreeView is
* an implementation of the {@link MultipleSelectionModel} abstract class.
* However, as noted in the API documentation for
* the {@link MultipleSelectionModel#selectionModeProperty() selectionMode}
* property, the default value is {@link SelectionMode#SINGLE}. To enable
* multiple selection in a default TreeView instance, it is therefore necessary
* to do the following:
*
*
{@code treeView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);}
*
* Customizing TreeView Visuals
* The visuals of the TreeView can be entirely customized by replacing the
* default {@link #cellFactoryProperty() cell factory}. A cell factory is used to
* generate {@link TreeCell} instances, which are used to represent an item in the
* TreeView. See the {@link Cell} class documentation for a more complete
* description of how to write custom Cells.
*
*
Warning: Nodes should not be inserted directly into the TreeView cells
* {@code TreeView} allows for it's cells to contain elements of any type, including
* {@code Node} instances. Putting nodes into
* the TreeView cells is strongly discouraged, as it can
* lead to unexpected results.
* Important points to note:
*
* - Avoid inserting {@code Node} instances directly into the {@code TreeView} cells or its data model.
* - The recommended approach is to put the relevant information into the items list, and
* provide a custom {@link #cellFactoryProperty() cell factory} to create the nodes for a
* given cell and update them on demand using the data stored in the item for that cell.
* - Avoid creating new {@code Node}s in the {@code updateItem} method of a custom {@link #cellFactoryProperty() cell factory}.
*
* The following minimal example shows how to create a custom cell factory for {@code TreeView} containing {@code Node}s:
*
*
{@code TreeItem treeRoot = new TreeItem<>();
* treeRoot.setExpanded(true);
* TreeView treeView = new TreeView<>(treeRoot);
*
* treeRoot.getChildren().addAll(
* new TreeItem<>(Color.RED),
* new TreeItem<>(Color.GREEN),
* new TreeItem<>(Color.BLUE));
*
* treeView.setCellFactory(p -> {
* return new TreeCell() {
* private final Rectangle rectangle;
* {
* setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
* rectangle = new Rectangle(10, 10);
* }
*
* @Override
* protected void updateItem(Color item, boolean empty) {
* super.updateItem(item, empty);
*
* if (item == null || empty) {
* setGraphic(null);
* } else {
* rectangle.setFill(item);
* setGraphic(rectangle);
* }
* }
* };});}
*
* This example has an anonymous custom {@code TreeCell} class in the custom cell factory.
* Note that the {@code Rectangle} ({@code Node}) object needs to be created in the instance initialization block
* or the constructor of the custom {@code TreeCell} class and updated/used in its {@code updateItem} method.
*
*
Editing
* This control supports inline editing of values, and this section attempts to
* give an overview of the available APIs and how you should use them.
*
* Firstly, cell editing most commonly requires a different user interface
* than when a cell is not being edited. This is the responsibility of the
* {@link Cell} implementation being used. For TreeView, this is the responsibility
* of the {@link #cellFactoryProperty() cell factory}. It is your choice whether the cell is
* permanently in an editing state (e.g. this is common for {@link CheckBox} cells),
* or to switch to a different UI when editing begins (e.g. when a double-click
* is received on a cell).
*
* To know when editing has been requested on a cell,
* simply override the {@link javafx.scene.control.Cell#startEdit()} method, and
* update the cell {@link javafx.scene.control.Cell#textProperty() text} and
* {@link javafx.scene.control.Cell#graphicProperty() graphic} properties as
* appropriate (e.g. set the text to null and set the graphic to be a
* {@link TextField}). Additionally, you should also override
* {@link Cell#cancelEdit()} to reset the UI back to its original visual state
* when the editing concludes. In both cases it is important that you also
* ensure that you call the super method to have the cell perform all duties it
* must do to enter or exit its editing mode.
*
* Once your cell is in an editing state, the next thing you are most probably
* interested in is how to commit or cancel the editing that is taking place. This is your
* responsibility as the cell factory provider. Your cell implementation will know
* when the editing is over, based on the user input (e.g. when the user presses
* the Enter or ESC keys on their keyboard). When this happens, it is your
* responsibility to call {@link Cell#commitEdit(Object)} or
* {@link Cell#cancelEdit()}, as appropriate.
*
* When you call {@link Cell#commitEdit(Object)} an event is fired to the
* TreeView, which you can observe by adding an {@link EventHandler} via
* {@link TreeView#setOnEditCommit(javafx.event.EventHandler)}. Similarly,
* you can also observe edit events for
* {@link TreeView#setOnEditStart(javafx.event.EventHandler) edit start}
* and {@link TreeView#setOnEditCancel(javafx.event.EventHandler) edit cancel}.
*
* By default the TreeView edit commit handler is non-null, with a default
* handler that attempts to overwrite the property value for the
* item in the currently-being-edited row. It is able to do this as the
* {@link Cell#commitEdit(Object)} method is passed in the new value, and this
* is passed along to the edit commit handler via the
* {@link EditEvent} that is fired. It is simply a matter of calling
* {@link EditEvent#getNewValue()} to retrieve this value.
*
*
It is very important to note that if you call
* {@link TreeView#setOnEditCommit(javafx.event.EventHandler)} with your own
* {@link EventHandler}, then you will be removing the default handler. Unless
* you then handle the writeback to the property (or the relevant data source),
* nothing will happen. You can work around this by using the
* {@link TreeView#addEventHandler(javafx.event.EventType, javafx.event.EventHandler)}
* method to add a {@link TreeView#editCommitEvent()} {@link EventType} with
* your desired {@link EventHandler} as the second argument. Using this method,
* you will not replace the default implementation, but you will be notified when
* an edit commit has occurred.
*
* Hopefully this summary answers some of the commonly asked questions.
* Fortunately, JavaFX ships with a number of pre-built cell factories that
* handle all the editing requirements on your behalf. You can find these
* pre-built cell factories in the javafx.scene.control.cell package.
*
* @see TreeItem
* @see TreeCell
* @param The type of the item contained within the {@link TreeItem} value
* property for all tree items in this TreeView.
* @since JavaFX 2.0
*/
@DefaultProperty("root")
public class TreeView extends Control {
/* *************************************************************************
* *
* Static properties and methods *
* *
**************************************************************************/
/**
* An EventType that indicates some edit event has occurred. It is the parent
* type of all other edit events: {@link #editStartEvent()},
* {@link #editCommitEvent()} and {@link #editCancelEvent()}.
*
* @param the type of the TreeItem instances used in this TreeView
* @return An EventType that indicates some edit event has occurred.
*/
@SuppressWarnings("unchecked")
public static EventType> editAnyEvent() {
return (EventType>) EDIT_ANY_EVENT;
}
private static final EventType> EDIT_ANY_EVENT =
new EventType<>(Event.ANY, "TREE_VIEW_EDIT");
/**
* An EventType used to indicate that an edit event has started within the
* TreeView upon which the event was fired.
*
* @param the type of the TreeItem instances used in this TreeView
* @return An EventType used to indicate that an edit event has started.
*/
@SuppressWarnings("unchecked")
public static EventType> editStartEvent() {
return (EventType>) EDIT_START_EVENT;
}
private static final EventType> EDIT_START_EVENT =
new EventType<>(editAnyEvent(), "EDIT_START");
/**
* An EventType used to indicate that an edit event has just been canceled
* within the TreeView upon which the event was fired.
*
* @param the type of the TreeItem instances used in this TreeView
* @return An EventType used to indicate that an edit event has just been
* canceled.
*/
@SuppressWarnings("unchecked")
public static EventType> editCancelEvent() {
return (EventType>) EDIT_CANCEL_EVENT;
}
private static final EventType> EDIT_CANCEL_EVENT =
new EventType<>(editAnyEvent(), "EDIT_CANCEL");
/**
* An EventType that is used to indicate that an edit in a TreeView has been
* committed. This means that user has made changes to the data of a
* TreeItem, and that the UI should be updated.
*
* @param the type of the TreeItem instances used in this TreeView
* @return An EventType that is used to indicate that an edit in a TreeView
* has been committed.
*/
@SuppressWarnings("unchecked")
public static EventType> editCommitEvent() {
return (EventType>) EDIT_COMMIT_EVENT;
}
private static final EventType> EDIT_COMMIT_EVENT =
new EventType<>(editAnyEvent(), "EDIT_COMMIT");
/**
* Returns the number of levels of 'indentation' of the given TreeItem,
* based on how many times {@link javafx.scene.control.TreeItem#getParent()}
* can be recursively called. If the TreeItem does not have any parent set,
* the returned value will be zero. For each time getParent() is recursively
* called, the returned value is incremented by one.
*
* Important note: This method is deprecated as it does
* not consider the root node. This means that this method will iterate
* past the root node of the TreeView control, if the root node has a parent.
* If this is important, call {@link TreeView#getTreeItemLevel(TreeItem)}
* instead.
*
* @param node The TreeItem for which the level is needed.
* @return An integer representing the number of parents above the given node,
* or -1 if the given TreeItem is null.
* @deprecated This method does not correctly calculate the distance from the
* given TreeItem to the root of the TreeView. As of JavaFX 8.0_20,
* the proper way to do this is via
* {@link TreeView#getTreeItemLevel(TreeItem)}
*/
@Deprecated(since="8u20")
public static int getNodeLevel(TreeItem> node) {
if (node == null) return -1;
int level = 0;
TreeItem> parent = node.getParent();
while (parent != null) {
level++;
parent = parent.getParent();
}
return level;
}
/* *************************************************************************
* *
* Constructors *
* *
**************************************************************************/
/**
* Creates an empty TreeView.
*
*
Refer to the {@link TreeView} class documentation for details on the
* default state of other properties.
*/
public TreeView() {
this(null);
}
/**
* Creates a TreeView with the provided root node.
*
*
Refer to the {@link TreeView} class documentation for details on the
* default state of other properties.
*
* @param root The node to be the root in this TreeView.
*/
public TreeView(TreeItem root) {
getStyleClass().setAll(DEFAULT_STYLE_CLASS);
setAccessibleRole(AccessibleRole.TREE_VIEW);
setRoot(root);
updateExpandedItemCount(root);
// install default selection and focus models - it's unlikely this will be changed
// by many users.
MultipleSelectionModel> sm = new TreeViewBitSetSelectionModel<>(this);
setSelectionModel(sm);
setFocusModel(new TreeViewFocusModel<>(this));
setOnEditCommit(DEFAULT_EDIT_COMMIT_HANDLER);
}
/* *************************************************************************
* *
* Instance Variables *
* *
**************************************************************************/
// used in the tree item modification event listener. Used by the
// layoutChildren method to determine whether the tree item count should
// be recalculated.
private boolean expandedItemCountDirty = true;
// Used in the getTreeItem(int row) method to act as a cache.
// See RT-26716 for the justification and performance gains.
private Map>> treeItemCacheMap = new HashMap<>();
/* *************************************************************************
* *
* Callbacks and Events *
* *
**************************************************************************/
// we use this to forward events that have bubbled up TreeItem instances
// to the TreeViewSkin, to force it to recalculate teh item count and redraw
// if necessary
private final EventHandler> rootEvent = e -> {
// this forces layoutChildren at the next pulse, and therefore
// updates the item count if necessary
EventType> eventType = e.getEventType();
boolean match = false;
while (eventType != null) {
if (eventType.equals(TreeItem.expandedItemCountChangeEvent())) {
match = true;
break;
}
eventType = eventType.getSuperType();
}
if (match) {
expandedItemCountDirty = true;
requestLayout();
}
};
private WeakEventHandler> weakRootEventListener;
/* *************************************************************************
* *
* Properties *
* *
**************************************************************************/
// --- Cell Factory
private ObjectProperty, TreeCell>> cellFactory;
/**
* Sets the cell factory that will be used for creating TreeCells,
* which are used to represent items in the
* TreeView. The factory works identically to the cellFactory in ListView
* and other complex composite controls. It is called to create a new
* TreeCell only when the system has determined that it doesn't have enough
* cells to represent the currently visible items. The TreeCell is reused
* by the system to represent different items in the tree when possible.
*
* Refer to the {@link Cell} class documentation for more details.
*
* @param value The {@link Callback} to use for generating TreeCell instances,
* or null if the default cell factory should be used.
*/
public final void setCellFactory(Callback, TreeCell> value) {
cellFactoryProperty().set(value);
}
/**
* Returns the cell factory that will be used for creating TreeCells,
* which are used to represent items in the TreeView, or null if no custom
* cell factory has been set.
* @return the cell factory
*/
public final Callback, TreeCell> getCellFactory() {
return cellFactory == null ? null : cellFactory.get();
}
/**
* Represents the cell factory that will be used for creating TreeCells,
* which are used to represent items in the TreeView.
* @return the cell factory property
*/
public final ObjectProperty, TreeCell>> cellFactoryProperty() {
if (cellFactory == null) {
cellFactory = new SimpleObjectProperty<>(this, "cellFactory");
}
return cellFactory;
}
// --- Root
private ObjectProperty> root = new SimpleObjectProperty<>(this, "root") {
private WeakReference> weakOldItem;
@Override protected void invalidated() {
TreeItem oldTreeItem = weakOldItem == null ? null : weakOldItem.get();
if (oldTreeItem != null && weakRootEventListener != null) {
oldTreeItem.removeEventHandler(TreeItem.treeNotificationEvent(), weakRootEventListener);
}
TreeItem root = getRoot();
if (root != null) {
weakRootEventListener = new WeakEventHandler<>(rootEvent);
getRoot().addEventHandler(TreeItem.treeNotificationEvent(), weakRootEventListener);
weakOldItem = new WeakReference<>(root);
}
// Fix for RT-37853
edit(null);
expandedItemCountDirty = true;
updateRootExpanded();
}
};
/**
* Sets the root node in this TreeView. See the {@link TreeItem} class level
* documentation for more details.
*
* @param value The {@link TreeItem} that will be placed at the root of the
* TreeView.
*/
public final void setRoot(TreeItem value) {
rootProperty().set(value);
}
/**
* Returns the current root node of this TreeView, or null if no root node
* is specified.
* @return The current root node, or null if no root node exists.
*/
public final TreeItem getRoot() {
return root == null ? null : root.get();
}
/**
* Property representing the root node of the TreeView.
* @return the root node property
*/
public final ObjectProperty> rootProperty() {
return root;
}
// --- Show Root
private BooleanProperty showRoot;
/**
* Specifies whether the root {@code TreeItem} should be shown within this
* TreeView.
*
* @param value If true, the root TreeItem will be shown, and if false it
* will be hidden.
*/
public final void setShowRoot(boolean value) {
showRootProperty().set(value);
}
/**
* Returns true if the root of the TreeView should be shown, and false if
* it should not. By default, the root TreeItem is visible in the TreeView.
* @return true if the root of the TreeView should be shown
*/
public final boolean isShowRoot() {
return showRoot == null ? true : showRoot.get();
}
/**
* Property that represents whether or not the TreeView root node is visible.
* @return the show root property
*/
public final BooleanProperty showRootProperty() {
if (showRoot == null) {
showRoot = new SimpleBooleanProperty(this, "showRoot", true) {
@Override protected void invalidated() {
updateRootExpanded();
updateExpandedItemCount(getRoot());
}
};
}
return showRoot;
}
// --- Selection Model
private ObjectProperty>> selectionModel;
/**
* Sets the {@link MultipleSelectionModel} to be used in the TreeView.
* Despite a TreeView requiring a MultipleSelectionModel
,
* it is possible to configure it to only allow single selection (see
* {@link MultipleSelectionModel#setSelectionMode(javafx.scene.control.SelectionMode)}
* for more information).
* @param value the {@link MultipleSelectionModel} to be used
*/
public final void setSelectionModel(MultipleSelectionModel> value) {
selectionModelProperty().set(value);
}
/**
* Returns the currently installed selection model.
* @return the currently installed selection model
*/
public final MultipleSelectionModel> getSelectionModel() {
return selectionModel == null ? null : selectionModel.get();
}
/**
* The SelectionModel provides the API through which it is possible
* to select single or multiple items within a TreeView, as well as inspect
* which rows have been selected by the user. Note that it has a generic
* type that must match the type of the TreeView itself.
* @return the selection model property
*/
public final ObjectProperty>> selectionModelProperty() {
if (selectionModel == null) {
selectionModel = new SimpleObjectProperty<>(this, "selectionModel");
}
return selectionModel;
}
// --- Focus Model
private ObjectProperty>> focusModel;
/**
* Sets the {@link FocusModel} to be used in the TreeView.
* @param value the {@link FocusModel} to be used
*/
public final void setFocusModel(FocusModel> value) {
focusModelProperty().set(value);
}
/**
* Returns the currently installed {@link FocusModel}.
* @return the currently installed {@link FocusModel}
*/
public final FocusModel> getFocusModel() {
return focusModel == null ? null : focusModel.get();
}
/**
* The FocusModel provides the API through which it is possible
* to control focus on zero or one rows of the TreeView. Generally the
* default implementation should be more than sufficient.
* @return the focus model property
*/
public final ObjectProperty>> focusModelProperty() {
if (focusModel == null) {
focusModel = new SimpleObjectProperty<>(this, "focusModel");
}
return focusModel;
}
// --- Expanded node count
/**
* Represents the number of tree nodes presently able to be visible in the
* TreeView. This is essentially the count of all expanded tree items, and
* their children.
*
*
For example, if just the root node is visible, the expandedItemCount will
* be one. If the root had three children and the root was expanded, the value
* will be four.
* @since JavaFX 8.0
*/
private ReadOnlyIntegerWrapper expandedItemCount = new ReadOnlyIntegerWrapper(this, "expandedItemCount", 0);
public final ReadOnlyIntegerProperty expandedItemCountProperty() {
return expandedItemCount.getReadOnlyProperty();
}
private void setExpandedItemCount(int value) {
expandedItemCount.set(value);
}
public final int getExpandedItemCount() {
if (expandedItemCountDirty) {
updateExpandedItemCount(getRoot());
}
return expandedItemCount.get();
}
// --- Fixed cell size
private DoubleProperty fixedCellSize;
/**
* Sets the new fixed cell size for this control. Any value greater than
* zero will enable fixed cell size mode, whereas a zero or negative value
* (or Region.USE_COMPUTED_SIZE) will be used to disabled fixed cell size
* mode.
*
* @param value The new fixed cell size value, or a value less than or equal
* to zero (or Region.USE_COMPUTED_SIZE) to disable.
* @since JavaFX 8.0
*/
public final void setFixedCellSize(double value) {
fixedCellSizeProperty().set(value);
}
/**
* Returns the fixed cell size value. A value less than or equal to zero is
* used to represent that fixed cell size mode is disabled, and a value
* greater than zero represents the size of all cells in this control.
*
* @return A double representing the fixed cell size of this control, or a
* value less than or equal to zero if fixed cell size mode is disabled.
* @since JavaFX 8.0
*/
public final double getFixedCellSize() {
return fixedCellSize == null ? Region.USE_COMPUTED_SIZE : fixedCellSize.get();
}
/**
* Specifies whether this control has cells that are a fixed height (of the
* specified value). If this value is less than or equal to zero,
* then all cells are individually sized and positioned. This is a slow
* operation. Therefore, when performance matters and developers are not
* dependent on variable cell sizes it is a good idea to set the fixed cell
* size value. Generally cells are around 24px, so setting a fixed cell size
* of 24 is likely to result in very little difference in visuals, but a
* improvement to performance.
*
*
To set this property via CSS, use the -fx-fixed-cell-size property.
* This should not be confused with the -fx-cell-size property. The difference
* between these two CSS properties is that -fx-cell-size will size all
* cells to the specified size, but it will not enforce that this is the
* only size (thus allowing for variable cell sizes, and preventing the
* performance gains from being possible). Therefore, when performance matters
* use -fx-fixed-cell-size, instead of -fx-cell-size. If both properties are
* specified in CSS, -fx-fixed-cell-size takes precedence.
*
* @return the fixed cell size property
* @since JavaFX 8.0
*/
public final DoubleProperty fixedCellSizeProperty() {
if (fixedCellSize == null) {
fixedCellSize = new StyleableDoubleProperty(Region.USE_COMPUTED_SIZE) {
@Override public CssMetaData,Number> getCssMetaData() {
return StyleableProperties.FIXED_CELL_SIZE;
}
@Override public Object getBean() {
return TreeView.this;
}
@Override public String getName() {
return "fixedCellSize";
}
};
}
return fixedCellSize;
}
// --- Editable
private BooleanProperty editable;
public final void setEditable(boolean value) {
editableProperty().set(value);
}
public final boolean isEditable() {
return editable == null ? false : editable.get();
}
/**
* Specifies whether this TreeView is editable - only if the TreeView and
* the TreeCells within it are both editable will a TreeCell be able to go
* into their editing state.
* @return the editable property
*/
public final BooleanProperty editableProperty() {
if (editable == null) {
editable = new SimpleBooleanProperty(this, "editable", false);
}
return editable;
}
// --- Editing Item
private ReadOnlyObjectWrapper> editingItem;
private void setEditingItem(TreeItem value) {
editingItemPropertyImpl().set(value);
}
/**
* Returns the TreeItem that is currently being edited in the TreeView,
* or null if no item is being edited.
* @return the TreeItem that is currently being edited in the TreeView
*/
public final TreeItem getEditingItem() {
return editingItem == null ? null : editingItem.get();
}
/**
* A property used to represent the TreeItem currently being edited
* in the TreeView, if editing is taking place, or null if no item is being edited.
*
*
It is not possible to set the editing item, instead it is required that
* you call {@link #edit(javafx.scene.control.TreeItem)}.
* @return the editing item property
*/
public final ReadOnlyObjectProperty> editingItemProperty() {
return editingItemPropertyImpl().getReadOnlyProperty();
}
private ReadOnlyObjectWrapper> editingItemPropertyImpl() {
if (editingItem == null) {
editingItem = new ReadOnlyObjectWrapper<>(this, "editingItem");
}
return editingItem;
}
// --- On Edit Start
private ObjectProperty>> onEditStart;
/**
* Sets the {@link EventHandler} that will be called when the user begins
* an edit.
* @param value the {@link EventHandler} that will be called when the user
* begins an edit
*/
public final void setOnEditStart(EventHandler> value) {
onEditStartProperty().set(value);
}
/**
* Returns the {@link EventHandler} that will be called when the user begins
* an edit.
* @return the {@link EventHandler} when the user begins an edit
*/
public final EventHandler> getOnEditStart() {
return onEditStart == null ? null : onEditStart.get();
}
/**
* This event handler will be fired when the user successfully initiates
* editing.
* @return the event handler when the user successfully initiates editing
*/
public final ObjectProperty>> onEditStartProperty() {
if (onEditStart == null) {
onEditStart = new SimpleObjectProperty<>(this, "onEditStart") {
@Override protected void invalidated() {
setEventHandler(TreeView.editStartEvent(), get());
}
};
}
return onEditStart;
}
// --- On Edit Commit
private ObjectProperty>> onEditCommit;
/**
* Sets the {@link EventHandler} that will be called when the user commits
* an edit.
* @param value the {@link EventHandler} that will be called when the user
* commits an edit
*/
public final void setOnEditCommit(EventHandler> value) {
onEditCommitProperty().set(value);
}
/**
* Returns the {@link EventHandler} that will be called when the user commits
* an edit.
* @return the {@link EventHandler} that will be called when the user commits
* an edit
*/
public final EventHandler> getOnEditCommit() {
return onEditCommit == null ? null : onEditCommit.get();
}
/**
* This property is used when the user performs an action that should
* result in their editing input being persisted.
*
* The EventHandler in this property should not be called directly -
* instead call {@link TreeCell#commitEdit(java.lang.Object)} from within
* your custom TreeCell. This will handle firing this event, updating the
* view, and switching out of the editing state.
* @return the event handler when the user performs an action that result in
* their editing input being persisted
*/
public final ObjectProperty>> onEditCommitProperty() {
if (onEditCommit == null) {
onEditCommit = new SimpleObjectProperty<>(this, "onEditCommit") {
@Override protected void invalidated() {
setEventHandler(TreeView.editCommitEvent(), get());
}
};
}
return onEditCommit;
}
private EventHandler> DEFAULT_EDIT_COMMIT_HANDLER = t -> {
TreeItem editedItem = t.getTreeItem();
if (editedItem == null) return;
editedItem.setValue(t.getNewValue());
};
// --- On Edit Cancel
private ObjectProperty>> onEditCancel;
/**
* Sets the {@link EventHandler} that will be called when the user cancels
* an edit.
* @param value the {@link EventHandler} that will be called when the user
* cancels an edit
*/
public final void setOnEditCancel(EventHandler> value) {
onEditCancelProperty().set(value);
}
/**
* Returns the {@link EventHandler} that will be called when the user cancels
* an edit.
* @return the {@link EventHandler} that will be called when the user cancels
* an edit
*/
public final EventHandler> getOnEditCancel() {
return onEditCancel == null ? null : onEditCancel.get();
}
/**
* This event handler will be fired when the user cancels editing a cell.
* @return the event handler will be fired when the user cancels editing a
* cell
*/
public final ObjectProperty>> onEditCancelProperty() {
if (onEditCancel == null) {
onEditCancel = new SimpleObjectProperty<>(this, "onEditCancel") {
@Override protected void invalidated() {
setEventHandler(TreeView.editCancelEvent(), get());
}
};
}
return onEditCancel;
}
/* *************************************************************************
* *
* Public API *
* *
**************************************************************************/
/** {@inheritDoc} */
@Override protected void layoutChildren() {
if (expandedItemCountDirty) {
updateExpandedItemCount(getRoot());
}
super.layoutChildren();
}
/**
* Instructs the TreeView to begin editing the given TreeItem, if
* the TreeView is {@link #editableProperty() editable}. Once
* this method is called, if the current
* {@link #cellFactoryProperty() cell factory} is set up to support editing,
* the Cell will switch its visual state to enable the user input to take place.
*
* @param item The TreeItem in the TreeView that should be edited.
*/
public void edit(TreeItem item) {
if (!isEditable()) return;
setEditingItem(item);
}
/**
* Scrolls the TreeView such that the item in the given index is visible to
* the end user.
*
* @param index The index that should be made visible to the user, assuming
* of course that it is greater than, or equal to 0, and less than the
* number of the visible items in the TreeView.
*/
public void scrollTo(int index) {
ControlUtils.scrollToIndex(this, index);
}
/**
* Called when there's a request to scroll an index into view using {@link #scrollTo(int)}
* @since JavaFX 8.0
*/
private ObjectProperty>> onScrollTo;
public void setOnScrollTo(EventHandler> value) {
onScrollToProperty().set(value);
}
public EventHandler> getOnScrollTo() {
if( onScrollTo != null ) {
return onScrollTo.get();
}
return null;
}
public ObjectProperty>> onScrollToProperty() {
if( onScrollTo == null ) {
onScrollTo = new ObjectPropertyBase<>() {
@Override
protected void invalidated() {
setEventHandler(ScrollToEvent.scrollToTopIndex(), get());
}
@Override
public Object getBean() {
return TreeView.this;
}
@Override
public String getName() {
return "onScrollTo";
}
};
}
return onScrollTo;
}
/**
* Returns the index position of the given TreeItem, assuming that it is
* currently accessible through the tree hierarchy (most notably, that all
* parent tree items are expanded). If a parent tree item is collapsed,
* the result is that this method will return -1 to indicate that the
* given tree item is not accessible in the tree.
*
* @param item The TreeItem for which the index is sought.
* @return An integer representing the location in the current TreeView of the
* first instance of the given TreeItem, or -1 if it is null or can not
* be found (for example, if a parent (all the way up to the root) is
* collapsed).
*/
public int getRow(TreeItem item) {
return TreeUtil.getRow(item, getRoot(), expandedItemCountDirty, isShowRoot());
}
/**
* Returns the TreeItem in the given index, or null if it is out of bounds.
*
* @param row The index of the TreeItem being sought.
* @return The TreeItem in the given index, or null if it is out of bounds.
*/
public TreeItem getTreeItem(int row) {
if (row < 0) return null;
// normalize the requested row based on whether showRoot is set
final int _row = isShowRoot() ? row : (row + 1);
if (expandedItemCountDirty) {
updateExpandedItemCount(getRoot());
} else {
if (treeItemCacheMap.containsKey(_row)) {
SoftReference> treeItemRef = treeItemCacheMap.get(_row);
TreeItem treeItem = treeItemRef.get();
if (treeItem != null) {
return treeItem;
}
}
}
TreeItem treeItem = TreeUtil.getItem(getRoot(), _row, expandedItemCountDirty);
treeItemCacheMap.put(_row, new SoftReference<>(treeItem));
return treeItem;
}
/**
* Returns the number of levels of 'indentation' of the given TreeItem,
* based on how many times getParent() can be recursively called. If the
* given TreeItem is the root node of this TreeView, or if the TreeItem does
* not have any parent set, the returned value will be zero. For each time
* getParent() is recursively called, the returned value is incremented by one.
*
* @param node The TreeItem for which the level is needed.
* @return An integer representing the number of parents above the given node,
* or -1 if the given TreeItem is null.
*/
public int getTreeItemLevel(TreeItem> node) {
final TreeItem> root = getRoot();
if (node == null) return -1;
if (node == root) return 0;
int level = 0;
TreeItem> parent = node.getParent();
while (parent != null) {
level++;
if (parent == root) {
break;
}
parent = parent.getParent();
}
return level;
}
/** {@inheritDoc} */
@Override protected Skin> createDefaultSkin() {
return new TreeViewSkin(this);
}
/**
* Calling {@code refresh()} forces the TreeView control to recreate and
* repopulate the cells necessary to populate the visual bounds of the control.
* In other words, this forces the TreeView to update what it is showing to
* the user. This is useful in cases where the underlying data source has
* changed in a way that is not observed by the TreeView itself.
*
* @since JavaFX 8u60
*/
public void refresh() {
getProperties().put(Properties.RECREATE, Boolean.TRUE);
}
/* *************************************************************************
* *
* Private Implementation *
* *
**************************************************************************/
private void updateExpandedItemCount(TreeItem treeItem) {
setExpandedItemCount(TreeUtil.updateExpandedItemCount(treeItem, expandedItemCountDirty, isShowRoot()));
if (expandedItemCountDirty) {
// this is a very inefficient thing to do, but for now having a cache
// is better than nothing at all...
treeItemCacheMap.clear();
}
expandedItemCountDirty = false;
}
private void updateRootExpanded() {
// if we aren't showing the root, and the root isn't expanded, we expand
// it now so that something is shown.
if (!isShowRoot() && getRoot() != null && ! getRoot().isExpanded()) {
getRoot().setExpanded(true);
}
}
/* *************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
private static final String DEFAULT_STYLE_CLASS = "tree-view";
private static class StyleableProperties {
private static final CssMetaData,Number> FIXED_CELL_SIZE =
new CssMetaData<>("-fx-fixed-cell-size",
SizeConverter.getInstance(),
Region.USE_COMPUTED_SIZE) {
@Override public Double getInitialValue(TreeView> node) {
return node.getFixedCellSize();
}
@Override public boolean isSettable(TreeView> n) {
return n.fixedCellSize == null || !n.fixedCellSize.isBound();
}
@Override public StyleableProperty getStyleableProperty(TreeView> n) {
return (StyleableProperty)n.fixedCellSizeProperty();
}
};
private static final List> STYLEABLES;
static {
final List> styleables =
new ArrayList<>(Control.getClassCssMetaData());
styleables.add(FIXED_CELL_SIZE);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
/**
* Gets the {@code CssMetaData} associated with this class, which may include the
* {@code CssMetaData} of its superclasses.
* @return the {@code CssMetaData}
* @since JavaFX 8.0
*/
public static List> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
/**
* {@inheritDoc}
* @since JavaFX 8.0
*/
@Override
public List> getControlCssMetaData() {
return getClassCssMetaData();
}
/* *************************************************************************
* *
* Accessibility handling *
* *
**************************************************************************/
/** {@inheritDoc} */
@Override
public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
switch (attribute) {
case MULTIPLE_SELECTION: {
MultipleSelectionModel> sm = getSelectionModel();
return sm != null && sm.getSelectionMode() == SelectionMode.MULTIPLE;
}
case ROW_COUNT: return getExpandedItemCount();
default: return super.queryAccessibleAttribute(attribute, parameters);
}
}
/* *************************************************************************
* *
* Support Interfaces *
* *
**************************************************************************/
/* *************************************************************************
* *
* Support Classes *
* *
**************************************************************************/
/**
* An {@link Event} subclass used specifically in TreeView for representing
* edit-related events. It provides additional API to easily access the
* TreeItem that the edit event took place on, as well as the input provided
* by the end user.
*
* @param The type of the input, which is the same type as the TreeView
* itself.
* @since JavaFX 2.0
*/
public static class EditEvent extends Event {
private static final long serialVersionUID = -4437033058917528976L;
/**
* Common supertype for all edit event types.
* @since JavaFX 8.0
*/
public static final EventType> ANY = EDIT_ANY_EVENT;
@SuppressWarnings("doclint:missing")
private final TreeView source;
@SuppressWarnings("doclint:missing")
private final T oldValue;
@SuppressWarnings("doclint:missing")
private final T newValue;
private transient final TreeItem treeItem;
/**
* Creates a new EditEvent instance to represent an edit event. This
* event is used for {@link #editStartEvent()},
* {@link #editCommitEvent()} and {@link #editCancelEvent()} types.
* @param source the source
* @param eventType the eventType
* @param treeItem the treeItem
* @param oldValue the oldValue
* @param newValue the newValue
*/
public EditEvent(TreeView source,
EventType extends EditEvent> eventType,
TreeItem treeItem, T oldValue, T newValue) {
super(source, Event.NULL_SOURCE_TARGET, eventType);
this.source = source;
this.oldValue = oldValue;
this.newValue = newValue;
this.treeItem = treeItem;
}
/**
* Returns the TreeView upon which the edit took place.
*/
@Override public TreeView getSource() {
return source;
}
/**
* Returns the {@link TreeItem} upon which the edit took place.
* @return the {@link TreeItem} upon which the edit took place
*/
public TreeItem getTreeItem() {
return treeItem;
}
/**
* Returns the new value input into the TreeItem by the end user.
* @return the new value input into the TreeItem by the end user
*/
public T getNewValue() {
return newValue;
}
/**
* Returns the old value that existed in the TreeItem prior to the current
* edit event.
* @return the old value that existed in the TreeItem prior to the current
* edit event
*/
public T getOldValue() {
return oldValue;
}
}
// package for testing
static class TreeViewBitSetSelectionModel extends MultipleSelectionModelBase> {
/* *********************************************************************
* *
* Internal fields *
* *
**********************************************************************/
private TreeView treeView = null;
/* *********************************************************************
* *
* Constructors *
* *
**********************************************************************/
public TreeViewBitSetSelectionModel(final TreeView treeView) {
if (treeView == null) {
throw new IllegalArgumentException("TreeView can not be null");
}
this.treeView = treeView;
this.treeView.rootProperty().addListener(weakRootPropertyListener);
showRootListener = o -> {
shiftSelection(0, treeView.isShowRoot() ? 1 : -1, null);
};
this.treeView.showRootProperty().addListener(new WeakInvalidationListener(showRootListener));
updateTreeEventListener(null, treeView.getRoot());
updateDefaultSelection();
}
private void updateTreeEventListener(TreeItem oldRoot, TreeItem newRoot) {
if (oldRoot != null && weakTreeItemListener != null) {
oldRoot.removeEventHandler(TreeItem.expandedItemCountChangeEvent(), weakTreeItemListener);
}
if (newRoot != null) {
//PENDING why create a new weak eventHandler?
weakTreeItemListener = new WeakEventHandler<>(treeItemListener);
newRoot.addEventHandler(TreeItem.expandedItemCountChangeEvent(), weakTreeItemListener);
}
}
private ChangeListener> rootPropertyListener = (observable, oldValue, newValue) -> {
updateDefaultSelection();
updateTreeEventListener(oldValue, newValue);
};
private EventHandler> treeItemListener = e -> {
if (getSelectedIndex() == -1 && getSelectedItem() == null) return;
final TreeItem treeItem = e.getTreeItem();
if (treeItem == null) return;
treeView.expandedItemCountDirty = true;
// we only shift selection from this row - everything before it
// is safe. We might change this below based on certain criteria
int startRow = treeView.getRow(treeItem);
int shift = 0;
ListChangeListener.Change extends TreeItem>> change = e.getChange();
if (change != null) {
change.next();
}
do {
final int addedSize = change == null ? 0 : change.getAddedSize();
final int removedSize = change == null ? 0 : change.getRemovedSize();
if (e.wasExpanded()) {
// need to shuffle selection by the number of visible children
shift += treeItem.getExpandedDescendentCount(false) - 1;
startRow++;
} else if (e.wasCollapsed()) {
// remove selection from any child treeItem, and also determine
// if any child item was selected (in which case the parent
// takes the selection on collapse)
treeItem.getExpandedDescendentCount(false);
final int count = treeItem.previousExpandedDescendentCount;
final int selectedIndex = getSelectedIndex();
final boolean wasPrimarySelectionInChild =
selectedIndex >= (startRow + 1) &&
selectedIndex < (startRow + count);
boolean wasAnyChildSelected = false;
selectedIndices._beginChange();
final int from = startRow + 1;
final int to = startRow + count;
final List removed = new ArrayList<>();
for (int i = from; i < to; i++) {
if (isSelected(i)) {
wasAnyChildSelected = true;
removed.add(i);
}
}
if (!removed.isEmpty()) {
selectedIndices._nextRemove(selectedIndices.indexOf(removed.get(0)), removed);
}
for (int index : removed) {
startAtomic();
clearSelection(index);
stopAtomic();
}
selectedIndices._endChange();
// put selection onto the newly-collapsed tree item
if (wasPrimarySelectionInChild && wasAnyChildSelected) {
select(startRow);
}
shift += -count + 1;
startRow++;
} else if (e.wasPermutated()) {
// no-op
} else if (e.wasAdded()) {
// shuffle selection by the number of added items
shift += ControlUtils.isTreeItemIncludingAncestorsExpanded(treeItem) ? addedSize : 0;
// RT-32963: We were taking the startRow from the TreeItem
// in which the children were added, rather than from the
// actual position of the new child. This led to selection
// being moved off the parent TreeItem by mistake.
// The 'if (e.getAddedSize() == 1)' condition here was
// subsequently commented out due to RT-33894.
startRow = treeView.getRow(e.getChange().getAddedSubList().get(0));
} else if (e.wasRemoved()) {
// the start row is incorrect - it is _not_ the index of the
// TreeItem in which the children were removed from (which is
// what it currently represents). We need to take the 'from'
// value out of the event and make use of that to understand
// what actually changed inside the children list.
startRow += e.getFrom() + 1;
// whilst we are here, we should check if the removed items
// are part of the selectedItems list - and remove them
// from selection if they are (as per RT-15446)
final List selectedIndices1 = getSelectedIndices();
final int selectedIndex = getSelectedIndex();
final List> selectedItems = getSelectedItems();
final TreeItem selectedItem = getSelectedItem();
final List extends TreeItem> removedChildren = e.getChange().getRemoved();
// shuffle selection by the number of removed items
// only if removed items are before the current selection.
if (ControlUtils.isTreeItemIncludingAncestorsExpanded(treeItem)) {
int lastSelectedSiblingIndex = selectedItems.stream()
.map(item -> ControlUtils.getIndexOfChildWithDescendant(treeItem, item))
.max(Comparator.naturalOrder())
.orElse(-1);
// shift only if the last selected sibling index is after the first removed child
if (e.getFrom() <= lastSelectedSiblingIndex || lastSelectedSiblingIndex == -1) {
shift -= removedSize;
}
}
for (int i = 0; i < selectedIndices1.size() && !selectedItems.isEmpty(); i++) {
int index = selectedIndices1.get(i);
if (index > selectedItems.size()) break;
if (removedChildren.size() == 1 &&
selectedItems.size() == 1 &&
selectedItem != null &&
selectedItem.equals(removedChildren.get(0))) {
// Bug fix for RT-28637
if (selectedIndex < getItemCount()) {
final int previousRow = selectedIndex == 0 ? 0 : selectedIndex - 1;
TreeItem newSelectedItem = getModelItem(previousRow);
if (!selectedItem.equals(newSelectedItem)) {
select(newSelectedItem);
}
}
}
}
}
} while (e.getChange() != null && e.getChange().next());
shiftSelection(startRow, shift, null);
if (e.wasAdded() || e.wasRemoved()) {
Integer anchor = TreeCellBehavior.getAnchor(treeView, null);
if (anchor != null && isSelected(anchor + shift)) {
TreeCellBehavior.setAnchor(treeView, anchor + shift, false);
}
}
};
private WeakChangeListener> weakRootPropertyListener =
new WeakChangeListener<>(rootPropertyListener);
private WeakEventHandler> weakTreeItemListener;
private InvalidationListener showRootListener;
/* *********************************************************************
* *
* Public selection API *
* *
**********************************************************************/
/** {@inheritDoc} */
@Override public void selectAll() {
// when a selectAll happens, the anchor should not change, so we store it
// before, and restore it afterwards
final int anchor = TreeCellBehavior.getAnchor(treeView, -1);
super.selectAll();
TreeCellBehavior.setAnchor(treeView, anchor, false);
}
/** {@inheritDoc} */
@Override public void select(TreeItem obj) {
// if (getRowCount() <= 0) return;
if (obj == null && getSelectionMode() == SelectionMode.SINGLE) {
clearSelection();
return;
}
// we firstly expand the path down such that the given object is
// visible. This fixes RT-14456, where selection was not happening
// correctly on TreeItems that are not visible.
if (obj != null) {
TreeItem> item = obj.getParent();
while (item != null) {
item.setExpanded(true);
item = item.getParent();
}
}
// Fix for RT-15419. We eagerly update the tree item count, such that
// selection occurs on the row
treeView.updateExpandedItemCount(treeView.getRoot());
// We have no option but to iterate through the model and select the
// first occurrence of the given object. Once we find the first one, we
// don't proceed to select any others.
int row = treeView.getRow(obj);
if (row == -1) {
// if we are here, we did not find the item in the entire data model.
// Even still, we allow for this item to be set to the give object.
// We expect that in concrete subclasses of this class we observe the
// data model such that we check to see if the given item exists in it,
// whilst SelectedIndex == -1 && SelectedItem != null.
setSelectedIndex(-1);
setSelectedItem(obj);
} else {
select(row);
}
}
/** {@inheritDoc} */
@Override public void clearAndSelect(int row) {
TreeCellBehavior.setAnchor(treeView, row, false);
super.clearAndSelect(row);
}
/* *********************************************************************
* *
* Support code *
* *
**********************************************************************/
/** {@inheritDoc} */
@Override protected void focus(int itemIndex) {
if (treeView.getFocusModel() != null) {
treeView.getFocusModel().focus(itemIndex);
}
// FIXME this is not the correct location for fire selection events (and does not take into account multiple selection)
treeView.notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM);
}
/** {@inheritDoc} */
@Override protected int getFocusedIndex() {
if (treeView.getFocusModel() == null) return -1;
return treeView.getFocusModel().getFocusedIndex();
}
/** {@inheritDoc} */
@Override protected int getItemCount() {
return treeView == null ? 0 : treeView.getExpandedItemCount();
}
/** {@inheritDoc} */
@Override public TreeItem getModelItem(int index) {
if (treeView == null) return null;
if (index < 0 || index >= treeView.getExpandedItemCount()) return null;
return treeView.getTreeItem(index);
}
/* *********************************************************************
* *
* Private implementation *
* *
**********************************************************************/
private void updateDefaultSelection() {
clearSelection();
// we put focus onto the first item, if there is at least
// one item in the list
focus(getItemCount() > 0 ? 0 : -1);
}
}
/**
*
* @param
*/
static class TreeViewFocusModel extends FocusModel> {
private final TreeView treeView;
public TreeViewFocusModel(final TreeView treeView) {
this.treeView = treeView;
this.treeView.rootProperty().addListener(weakRootPropertyListener);
updateTreeEventListener(null, treeView.getRoot());
if (treeView.getExpandedItemCount() > 0) {
focus(0);
}
showRootListener = obs -> {
if (isFocused(0)) {
focus(-1);
focus(0);
}
};
treeView.showRootProperty().addListener(new WeakInvalidationListener(showRootListener));
focusedIndexProperty().addListener(o -> {
treeView.notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM);
});
}
private final ChangeListener> rootPropertyListener = (observable, oldValue, newValue) -> {
updateTreeEventListener(oldValue, newValue);
};
private final WeakChangeListener> weakRootPropertyListener =
new WeakChangeListener<>(rootPropertyListener);
private final InvalidationListener showRootListener;
private void updateTreeEventListener(TreeItem oldRoot, TreeItem newRoot) {
if (oldRoot != null && weakTreeItemListener != null) {
oldRoot.removeEventHandler(TreeItem.expandedItemCountChangeEvent(), weakTreeItemListener);
}
if (newRoot != null) {
weakTreeItemListener = new WeakEventHandler<>(treeItemListener);
newRoot.addEventHandler(TreeItem.expandedItemCountChangeEvent(), weakTreeItemListener);
}
}
private EventHandler> treeItemListener = new EventHandler<>() {
@Override public void handle(TreeModificationEvent e) {
// don't shift focus if the event occurred on a tree item after
// the focused row, or if there is no focus index at present
if (getFocusedIndex() == -1) return;
int row = treeView.getRow(e.getTreeItem());
int shift = 0;
if (e.getChange() != null) {
e.getChange().next();
}
do {
if (e.wasExpanded()) {
if (row < getFocusedIndex()) {
// need to shuffle selection by the number of visible children
shift += e.getTreeItem().getExpandedDescendentCount(false) - 1;
}
} else if (e.wasCollapsed()) {
if (row < getFocusedIndex()) {
// need to shuffle selection by the number of visible children
// that were just hidden
shift += -e.getTreeItem().previousExpandedDescendentCount + 1;
}
} else if (e.wasAdded()) {
// get the TreeItem the event occurred on - we only need to
// shift if the tree item is expanded
TreeItem eventTreeItem = e.getTreeItem();
if (ControlUtils.isTreeItemIncludingAncestorsExpanded(eventTreeItem)) {
for (int i = 0; i < e.getAddedChildren().size(); i++) {
// get the added item and determine the row it is in
TreeItem item = e.getAddedChildren().get(i);
row = treeView.getRow(item);
if (item != null && row <= (shift+getFocusedIndex())) {
shift += item.getExpandedDescendentCount(false);
}
}
}
} else if (e.wasRemoved()) {
row += e.getFrom() + 1;
for (int i = 0; i < e.getRemovedChildren().size(); i++) {
TreeItem item = e.getRemovedChildren().get(i);
if (item != null && item.equals(getFocusedItem())) {
focus(Math.max(0, getFocusedIndex() - 1));
return;
}
}
if (ControlUtils.isTreeItemIncludingAncestorsExpanded(e.getTreeItem())) {
int focusedSiblingRow = ControlUtils.getIndexOfChildWithDescendant(e.getTreeItem(), getFocusedItem());
if (e.getFrom() <= focusedSiblingRow) {
// shuffle selection by the number of removed items
shift -= e.getRemovedSize();
}
}
}
} while (e.getChange() != null && e.getChange().next());
if(shift != 0) {
final int newFocus = getFocusedIndex() + shift;
if (newFocus >= 0) {
Platform.runLater(() -> focus(newFocus));
}
}
}
};
private WeakEventHandler> weakTreeItemListener;
@Override protected int getItemCount() {
return treeView == null ? -1 : treeView.getExpandedItemCount();
}
@Override protected TreeItem getModelItem(int index) {
if (treeView == null) return null;
if (index < 0 || index >= treeView.getExpandedItemCount()) return null;
return treeView.getTreeItem(index);
}
/** {@inheritDoc} */
@Override public void focus(int index) {
if (treeView.expandedItemCountDirty) {
treeView.updateExpandedItemCount(treeView.getRoot());
}
super.focus(index);
}
}
}