javafx.scene.control.TreeCell Maven / Gradle / Ivy
/*
* Copyright (c) 2011, 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.PseudoClass;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ListChangeListener;
import javafx.scene.AccessibleAction;
import javafx.scene.AccessibleAttribute;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.scene.control.skin.TreeCellSkin;
import javafx.collections.WeakListChangeListener;
import java.lang.ref.WeakReference;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
/**
* The {@link Cell} type used with the {@link TreeView} control. In addition to
* the API defined on {@link IndexedCell}, the TreeCell
* exposes additional states and pseudo classes for use by CSS.
*
* A TreeCell watches the selection model of the TreeView for which it is
* associated, ensuring that it visually indicates to the user whether it is
* selected. When a TreeCell is selected, this is exposed both via the
* {@link #selectedProperty() selected} property, as well as via the 'selected'
* CSS pseudo class state.
*
* Due to the fact that TreeCell extends from {@link IndexedCell}, each TreeCell
* also provides an {@link #indexProperty() index} property. The index will be
* updated as cells are expanded and collapsed, and therefore should be
* considered a view index rather than a model index.
*
* Finally, each TreeCell also has a reference back to the TreeView that it is
* being used with. Each TreeCell belongs to one and only one TreeView.
*
* @see TreeView
* @see TreeItem
* @param The type of the value contained within the
* {@link #treeItemProperty() TreeItem} property.
* @since JavaFX 2.0
*/
public class TreeCell extends IndexedCell {
/* *************************************************************************
* *
* Constructors *
* *
**************************************************************************/
/**
* Creates a default TreeCell instance.
*/
public TreeCell() {
getStyleClass().addAll(DEFAULT_STYLE_CLASS);
setAccessibleRole(AccessibleRole.TREE_ITEM);
}
/* *************************************************************************
* *
* Callbacks and events *
* *
**************************************************************************/
private final ListChangeListener selectedListener = c -> {
updateSelection();
};
/**
* Listens to the selectionModel property on the TreeView. Whenever the entire model is changed,
* we have to unhook the weakSelectedListener and update the selection.
*/
private final ChangeListener>> selectionModelPropertyListener = new ChangeListener<>() {
@Override public void changed(ObservableValue extends MultipleSelectionModel>> observable,
MultipleSelectionModel> oldValue,
MultipleSelectionModel> newValue) {
if (oldValue != null) {
oldValue.getSelectedIndices().removeListener(weakSelectedListener);
}
if (newValue != null) {
newValue.getSelectedIndices().addListener(weakSelectedListener);
}
updateSelection();
}
};
private final InvalidationListener focusedListener = valueModel -> {
updateFocus();
};
/**
* Listens to the focusModel property on the TreeView. Whenever the entire model is changed,
* we have to unhook the weakFocusedListener and update the focus.
*/
private final ChangeListener>> focusModelPropertyListener = new ChangeListener<>() {
@Override public void changed(ObservableValue extends FocusModel>> observable,
FocusModel> oldValue,
FocusModel> newValue) {
if (oldValue != null) {
oldValue.focusedIndexProperty().removeListener(weakFocusedListener);
}
if (newValue != null) {
newValue.focusedIndexProperty().addListener(weakFocusedListener);
}
updateFocus();
}
};
private final InvalidationListener editingListener = valueModel -> {
updateEditing();
};
private final InvalidationListener leafListener = new InvalidationListener() {
@Override public void invalidated(Observable valueModel) {
// necessary to update the disclosure node in the skin when the
// leaf property changes
TreeItem treeItem = getTreeItem();
if (treeItem != null) {
requestLayout();
}
}
};
/* proxy pseudo-class state change from treeItem's expandedProperty */
private boolean oldIsExpanded;
private final InvalidationListener treeItemExpandedInvalidationListener = new InvalidationListener() {
@Override public void invalidated(Observable o) {
boolean isExpanded = ((BooleanProperty)o).get();
pseudoClassStateChanged(EXPANDED_PSEUDOCLASS_STATE, isExpanded);
pseudoClassStateChanged(COLLAPSED_PSEUDOCLASS_STATE, !isExpanded);
if (isExpanded != oldIsExpanded) {
notifyAccessibleAttributeChanged(AccessibleAttribute.EXPANDED);
}
oldIsExpanded = isExpanded;
}
};
private final InvalidationListener rootPropertyListener = observable -> {
updateItem(-1);
};
private final WeakListChangeListener weakSelectedListener = new WeakListChangeListener<>(selectedListener);
private final WeakChangeListener>> weakSelectionModelPropertyListener = new WeakChangeListener<>(selectionModelPropertyListener);
private final WeakInvalidationListener weakFocusedListener = new WeakInvalidationListener(focusedListener);
private final WeakChangeListener>> weakFocusModelPropertyListener = new WeakChangeListener<>(focusModelPropertyListener);
private final WeakInvalidationListener weakEditingListener = new WeakInvalidationListener(editingListener);
private final WeakInvalidationListener weakLeafListener = new WeakInvalidationListener(leafListener);
private final WeakInvalidationListener weakTreeItemExpandedInvalidationListener =
new WeakInvalidationListener(treeItemExpandedInvalidationListener);
private final WeakInvalidationListener weakRootPropertyListener = new WeakInvalidationListener(rootPropertyListener);
/* *************************************************************************
* *
* Properties *
* *
**************************************************************************/
// --- TreeItem
private ReadOnlyObjectWrapper> treeItem =
new ReadOnlyObjectWrapper<>(this, "treeItem") {
TreeItem oldValue = null;
@Override protected void invalidated() {
if (oldValue != null) {
oldValue.expandedProperty().removeListener(weakTreeItemExpandedInvalidationListener);
}
oldValue = get();
if (oldValue != null) {
oldIsExpanded = oldValue.isExpanded();
oldValue.expandedProperty().addListener(weakTreeItemExpandedInvalidationListener);
// fake an invalidation to ensure updated pseudo-class state
weakTreeItemExpandedInvalidationListener.invalidated(oldValue.expandedProperty());
}
}
};
private void setTreeItem(TreeItem value) {
treeItem.set(value);
}
/**
* Returns the TreeItem currently set in this TreeCell.
* @return the TreeItem currently set in this TreeCell
*/
public final TreeItem getTreeItem() { return treeItem.get(); }
/**
* Each TreeCell represents at most a single {@link TreeItem}, which is
* represented by this property.
* @return the TreeItem property representing this TreeCell
*/
public final ReadOnlyObjectProperty> treeItemProperty() { return treeItem.getReadOnlyProperty(); }
// --- Disclosure Node
private ObjectProperty disclosureNode = new SimpleObjectProperty<>(this, "disclosureNode");
/**
* The node to use as the "disclosure" triangle, or toggle, used for
* expanding and collapsing items. This is only used in the case of
* an item in the tree which contains child items. If not specified, the
* TreeCell's Skin implementation is responsible for providing a default
* disclosure node.
* @param value the disclosure node
*/
public final void setDisclosureNode(Node value) { disclosureNodeProperty().set(value); }
/**
* Returns the current disclosure node set in this TreeCell.
* @return the current disclosure node set in this TreeCell
*/
public final Node getDisclosureNode() { return disclosureNode.get(); }
/**
* The disclosure node is commonly seen represented as a triangle that rotates
* on screen to indicate whether or not the TreeItem that it is placed
* beside is expanded or collapsed.
* @return the disclosure node
*/
public final ObjectProperty disclosureNodeProperty() { return disclosureNode; }
// --- TreeView
private ReadOnlyObjectWrapper> treeView = new ReadOnlyObjectWrapper<>() {
private WeakReference> weakTreeViewRef;
@Override protected void invalidated() {
MultipleSelectionModel> sm;
FocusModel> fm;
if (weakTreeViewRef != null) {
TreeView oldTreeView = weakTreeViewRef.get();
if (oldTreeView != null) {
// remove old listeners
sm = oldTreeView.getSelectionModel();
if (sm != null) {
sm.getSelectedIndices().removeListener(weakSelectedListener);
}
fm = oldTreeView.getFocusModel();
if (fm != null) {
fm.focusedIndexProperty().removeListener(weakFocusedListener);
}
oldTreeView.editingItemProperty().removeListener(weakEditingListener);
oldTreeView.focusModelProperty().removeListener(weakFocusModelPropertyListener);
oldTreeView.selectionModelProperty().removeListener(weakSelectionModelPropertyListener);
oldTreeView.rootProperty().removeListener(weakRootPropertyListener);
}
weakTreeViewRef = null;
}
TreeView treeView = get();
if (treeView != null) {
sm = treeView.getSelectionModel();
if (sm != null) {
// listening for changes to treeView.selectedIndex and IndexedCell.index,
// to determine if this cell is selected
sm.getSelectedIndices().addListener(weakSelectedListener);
}
fm = treeView.getFocusModel();
if (fm != null) {
// similar to above, but this time for focus
fm.focusedIndexProperty().addListener(weakFocusedListener);
}
treeView.editingItemProperty().addListener(weakEditingListener);
treeView.focusModelProperty().addListener(weakFocusModelPropertyListener);
treeView.selectionModelProperty().addListener(weakSelectionModelPropertyListener);
treeView.rootProperty().addListener(weakRootPropertyListener);
weakTreeViewRef = new WeakReference<>(treeView);
}
updateItem(-1);
requestLayout();
}
@Override
public Object getBean() {
return TreeCell.this;
}
@Override
public String getName() {
return "treeView";
}
};
private void setTreeView(TreeView value) { treeView.set(value); }
/**
* Returns the TreeView associated with this TreeCell.
* @return the TreeView associated with this TreeCell
*/
public final TreeView getTreeView() { return treeView.get(); }
/**
* A TreeCell is explicitly linked to a single {@link TreeView} instance,
* which is represented by this property.
* @return the TreeView property of this TreeCell
*/
public final ReadOnlyObjectProperty> treeViewProperty() { return treeView.getReadOnlyProperty(); }
/* *************************************************************************
* *
* Public API *
* *
**************************************************************************/
// treeItem at time of startEdit - fix for JDK-8267094
private TreeItem treeItemAtStartEdit;
/** {@inheritDoc} */
@Override public void startEdit() {
if (isEditing()) return;
final TreeView tree = getTreeView();
if (! isEditable() || (tree != null && ! tree.isEditable())) {
// if (Logging.getControlsLogger().isLoggable(PlatformLogger.SEVERE)) {
// Logging.getControlsLogger().severe(
// "Can not call TreeCell.startEdit() on this TreeCell, as it "
// + "is not allowed to enter its editing state (TreeCell: "
// + this + ", TreeView: " + tree + ").");
// }
return;
}
updateItem(-1);
// it makes sense to get the cell into its editing state before firing
// the event to the TreeView below, so that's what we're doing here
// by calling super.startEdit().
super.startEdit();
if (!isEditing()) return;
treeItemAtStartEdit = getTreeItem();
// Inform the TreeView of the edit starting.
if (tree != null) {
tree.fireEvent(new TreeView.EditEvent<>(tree,
TreeView.editStartEvent(),
treeItemAtStartEdit,
getItem(),
null));
tree.edit(treeItemAtStartEdit);
tree.requestFocus();
}
}
/** {@inheritDoc} */
@Override public void commitEdit(T newValue) {
if (! isEditing()) return;
// inform parent classes of the commit, so that they can switch us
// out of the editing state.
// This MUST come before the updateItem call below, otherwise it will
// call cancelEdit(), resulting in both commit and cancel events being
// fired (as identified in RT-29650)
super.commitEdit(newValue);
final TreeItem treeItem = getTreeItem();
final TreeView tree = getTreeView();
boolean treeShouldRequestFocus = false;
// JDK-8187307: fire the commit after updating cell's editing state
if (tree != null) {
// The cell is going to be updated, and the current focus owner might be removed from it.
// Before that happens, check if it has the tree as a parent (otherwise the user might have
// clicked out of the tree entirely and given focus to something else), so the tree can
// request the focus back, once the edit commit ends.
treeShouldRequestFocus = ControlUtils.controlShouldRequestFocusIfCurrentFocusOwnerIsChild(tree);
// Inform the TreeView of the edit being ready to be committed.
tree.fireEvent(new TreeView.EditEvent<>(tree,
TreeView.editCommitEvent(),
treeItem,
getItem(),
newValue));
}
// Update the item within this cell, so that it represents the new value
updateItem(-1);
if (tree != null) {
// reset the editing item in the TreeView
tree.edit(null);
// request focus back onto the tree, only if the current focus
// owner had the tree as a parent.
// It would be rude of us to request it back again.
if (treeShouldRequestFocus) {
tree.requestFocus();
}
}
treeItemAtStartEdit = null;
}
/** {@inheritDoc} */
@Override public void cancelEdit() {
if (! isEditing()) return;
TreeView tree = getTreeView();
super.cancelEdit();
if (tree != null) {
TreeItem editingItem = treeItemAtStartEdit;
T value = editingItem != null ? editingItem.getValue() : null;
// reset the editing index on the TreeView
if (updateEditingIndex) tree.edit(null);
// request focus back onto the tree, only if the current focus
// owner has the tree as a parent (otherwise the user might have
// clicked out of the tree entirely and given focus to something else).
// It would be rude of us to request it back again.
ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(tree);
tree.fireEvent(new TreeView.EditEvent<>(tree,
TreeView.editCancelEvent(),
editingItem,
value,
null));
}
treeItemAtStartEdit = null;
}
/** {@inheritDoc} */
@Override protected Skin> createDefaultSkin() {
return new TreeCellSkin<>(this);
}
/* *************************************************************************
* *
* Private Implementation *
* *
**************************************************************************/
/** {@inheritDoc} */
@Override void indexChanged(int oldIndex, int newIndex) {
super.indexChanged(oldIndex, newIndex);
// when the cell index changes, this may result in the cell
// changing state to be selected and/or focused.
if (isEditing() && newIndex == oldIndex) {
// no-op
// Fix for RT-31165 - if we (needlessly) update the index whilst the
// cell is being edited it will no longer be in an editing state.
// This means that in certain (common) circumstances that it will
// appear that a cell is uneditable as, despite being clicked, it
// will not change to the editing state as a layout of VirtualFlow
// is immediately invoked, which forces all cells to be updated.
} else {
updateItem(oldIndex);
updateSelection();
updateFocus();
updateEditing();
}
}
private boolean isFirstRun = true;
private void updateItem(int oldIndex) {
TreeView tv = getTreeView();
if (tv == null) return;
// Compute whether the index for this cell is for a real item
int index = getIndex();
boolean valid = index >=0 && index < tv.getExpandedItemCount();
final boolean isEmpty = isEmpty();
final TreeItem oldTreeItem = getTreeItem();
// Cause the cell to update itself
outer: if (valid) {
// update the TreeCell state.
// get the new treeItem that is about to go in to the TreeCell
TreeItem newTreeItem = tv.getTreeItem(index);
T newValue = newTreeItem == null ? null : newTreeItem.getValue();
T oldValue = oldTreeItem == null ? null : oldTreeItem.getValue();
// For the sake of RT-14279, it is important that the order of these
// method calls is as shown below. If the order is switched, it is
// likely that events will be fired where the item is null, even
// though calling cell.getTreeItem().getValue() returns the value
// as expected
// RT-35864 - if the index didn't change, then avoid calling updateItem
// unless the item has changed.
if (oldIndex == index) {
if (!isItemChanged(oldValue, newValue)) {
// RT-37054: we break out of the if/else code here and
// proceed with the code following this, so that we may
// still update references, listeners, etc as required.
break outer;
}
}
updateTreeItem(newTreeItem);
updateItem(newValue, false);
} else {
// RT-30484 We need to allow a first run to be special-cased to allow
// for the updateItem method to be called at least once to allow for
// the correct visual state to be set up. In particular, in RT-30484
// refer to Ensemble8PopUpTree.png - in this case the arrows are being
// shown as the new cells are instantiated with the arrows in the
// children list, and are only hidden in updateItem.
if ((!isEmpty && oldTreeItem != null) || isFirstRun) {
updateTreeItem(null);
updateItem(null, true);
isFirstRun = false;
}
}
}
private void updateSelection() {
if (isEmpty()) return;
if (getIndex() == -1 || getTreeView() == null) return;
SelectionModel> sm = getTreeView().getSelectionModel();
if (sm == null) {
updateSelected(false);
return;
}
boolean isSelected = sm.isSelected(getIndex());
if (isSelected() == isSelected) return;
updateSelected(isSelected);
}
private void updateFocus() {
if (getIndex() == -1 || getTreeView() == null) return;
FocusModel> fm = getTreeView().getFocusModel();
if (fm == null) {
setFocused(false);
return;
}
setFocused(fm.isFocused(getIndex()));
}
private boolean updateEditingIndex = true;
private void updateEditing() {
final int index = getIndex();
final TreeView tree = getTreeView();
final TreeItem treeItem = getTreeItem();
final TreeItem editItem = tree == null ? null : tree.getEditingItem();
final boolean editing = isEditing();
if (index == -1 || tree == null || treeItem == null) {
if (editing) {
// JDK-8265210: must cancel edit if index changed to -1 by re-use
doCancelEditing();
}
return;
}
final boolean match = treeItem.equals(editItem);
// If my tree item is the item being edited and I'm not currently in
// the edit mode, then I need to enter the edit mode
if (match && !editing) {
startEdit();
} else if (! match && editing) {
doCancelEditing();
}
}
private void doCancelEditing() {
// If my tree item is not the one being edited then I need to cancel
// the edit. The tricky thing here is that as part of this call
// I cannot end up calling tree.edit(null) the way that the standard
// cancelEdit method would do. Yet, I need to call cancelEdit
// so that subclasses which override cancelEdit can execute. So,
// I have to use a kind of hacky flag workaround.
try {
// try-finally to make certain that the flag is reliably reset to true
updateEditingIndex = false;
cancelEdit();
} finally {
updateEditingIndex = true;
}
}
/* *************************************************************************
* *
* Expert API *
* *
**************************************************************************/
/**
* Updates the TreeView associated with this TreeCell.
*
* @param tree The new TreeView that should be associated with this TreeCell.
* Note: This function is intended to be used by experts, primarily
* by those implementing new Skins. It is not common
* for developers or designers to access this function directly.
*/
public final void updateTreeView(TreeView tree) {
setTreeView(tree);
}
/**
* Updates the TreeItem associated with this TreeCell.
*
* @param treeItem The new TreeItem that should be associated with this
* TreeCell.
* Note: This function is intended to be used by experts, primarily
* by those implementing new Skins. It is not common
* for developers or designers to access this function directly.
*/
public final void updateTreeItem(TreeItem treeItem) {
TreeItem _treeItem = getTreeItem();
if (_treeItem != null) {
_treeItem.leafProperty().removeListener(weakLeafListener);
}
setTreeItem(treeItem);
if (treeItem != null) {
treeItem.leafProperty().addListener(weakLeafListener);
}
}
/* *************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
private static final String DEFAULT_STYLE_CLASS = "tree-cell";
private static final PseudoClass EXPANDED_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("expanded");
private static final PseudoClass COLLAPSED_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("collapsed");
/* *************************************************************************
* *
* Accessibility handling *
* *
**************************************************************************/
/** {@inheritDoc} */
@Override
public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
TreeItem treeItem = getTreeItem();
TreeView treeView = getTreeView();
switch (attribute) {
case TREE_ITEM_PARENT: {
if (treeView == null) return null;
if (treeItem == null) return null;
TreeItem parent = treeItem.getParent();
if (parent == null) return null;
int parentIndex = treeView.getRow(parent);
return treeView.queryAccessibleAttribute(AccessibleAttribute.ROW_AT_INDEX, parentIndex);
}
case TREE_ITEM_COUNT: {
if (treeItem == null) return 0;
if (!treeItem.isExpanded()) return 0;
return treeItem.getChildren().size();
}
case TREE_ITEM_AT_INDEX: {
if (treeItem == null) return null;
if (!treeItem.isExpanded()) return null;
int index = (Integer)parameters[0];
if (index >= treeItem.getChildren().size()) return null;
TreeItem child = treeItem.getChildren().get(index);
if (child == null) return null;
int childIndex = treeView.getRow(child);
return treeView.queryAccessibleAttribute(AccessibleAttribute.ROW_AT_INDEX, childIndex);
}
case LEAF: return treeItem == null ? true : treeItem.isLeaf();
case EXPANDED: return treeItem == null ? false : treeItem.isExpanded();
case INDEX: return getIndex();
case SELECTED: return isSelected();
case DISCLOSURE_LEVEL: {
return treeView == null ? 0 : treeView.getTreeItemLevel(treeItem);
}
default: return super.queryAccessibleAttribute(attribute, parameters);
}
}
/** {@inheritDoc} */
@Override
public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
switch (action) {
case EXPAND: {
TreeItem treeItem = getTreeItem();
if (treeItem != null) treeItem.setExpanded(true);
break;
}
case COLLAPSE: {
TreeItem treeItem = getTreeItem();
if (treeItem != null) treeItem.setExpanded(false);
break;
}
case REQUEST_FOCUS: {
TreeView treeView = getTreeView();
if (treeView != null) {
FocusModel> fm = treeView.getFocusModel();
if (fm != null) {
fm.focus(getIndex());
}
}
break;
}
default: super.executeAccessibleAction(action);
}
}
}