>) 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));
sceneProperty().addListener((o, oldScene, newScene) -> {
if (oldScene != null) {
oldScene.focusOwnerProperty().removeListener(weakFocusOwnerListener);
}
if (newScene != null) {
newScene.focusOwnerProperty().addListener(weakFocusOwnerListener);
}
});
}
/***************************************************************************
* *
* 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 InvalidationListener focusOwnerListener = o -> {
if (!ControlUtils.isFocusOnNodeOrAnyChild(this)) {
edit(null);
}
};
private WeakInvalidationListener weakFocusOwnerListener = new WeakInvalidationListener(focusOwnerListener);
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, TreeCell>>(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;
}
// --- 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,Number>("-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)(WritableValue) n.fixedCellSizeProperty();
}
};
private static final List> STYLEABLES;
static {
final List> styleables =
new ArrayList>(Control.getClassCssMetaData());
styleables.add(FIXED_CELL_SIZE);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
/**
* @return The CssMetaData associated with this class, which may include the
* CssMetaData of its superclasses.
* @since JavaFX 8.0
*/
public static List> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
/**
* {@inheritDoc}
* @since JavaFX 8.0
*/
@Override
public List> getControlCssMetaData() {
return getClassCssMetaData();
}
/***************************************************************************
* *
* Accessibility handling *
* *
**************************************************************************/
@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;
private final TreeView source;
private final T oldValue;
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 #EDIT_START_EVENT},
* {@link #EDIT_COMMIT_EVENT} and {@link #EDIT_CANCEL_EVENT} 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);
this.treeView.showRootProperty().addListener(o -> {
shiftSelection(0, treeView.isShowRoot() ? 1 : -1, null);
});
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) {
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);
}
}
ControlUtils.reducingChange(selectedIndices, 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 += treeItem.isExpanded() ? 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()) {
// shuffle selection by the number of removed items
shift += treeItem.isExpanded() ? -removedSize : 0;
// 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();
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;
/***********************************************************************
* *
* 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);
}
treeView.showRootProperty().addListener(o -> {
if (isFocused(0)) {
focus(-1);
focus(0);
}
});
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 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 (eventTreeItem.isExpanded()) {
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 (row <= getFocusedIndex()) {
// shuffle selection by the number of removed items
shift += e.getTreeItem().isExpanded() ? -e.getRemovedSize() : 0;
}
}
} 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);
}
}
}