javafx.scene.control.cell.CheckBoxTreeCell Maven / Gradle / Ivy
/*
* Copyright (c) 2012, 2023, 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.cell;
import javafx.scene.AccessibleAttribute;
import javafx.scene.AccessibleAttribute.ToggleState;
import javafx.scene.AccessibleRole;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.util.Callback;
import javafx.util.StringConverter;
/**
* A class containing a {@link TreeCell} implementation that draws a
* {@link CheckBox} node inside the cell, along with support for common
* interactions (discussed in more depth shortly).
*
* To make creating TreeViews with CheckBoxes easier, a convenience class
* called {@link CheckBoxTreeItem} is provided. It is highly recommended
* that developers use this class, rather than the regular {@link TreeItem}
* class, when constructing their TreeView tree structures. Refer to the
* CheckBoxTreeItem API documentation for an example on how these two classes
* can be combined.
*
*
When used in a TreeView, the CheckBoxCell is rendered with a CheckBox to
* the right of the 'disclosure node' (i.e. the arrow). The item stored in
* {@link CheckBoxTreeItem#getValue()} will then have the StringConverter called
* on it, and this text will take all remaining horizontal space. Additionally,
* by using {@link CheckBoxTreeItem}, the TreeView will automatically handle
* situations such as:
*
*
* - Clicking on the {@link CheckBox} beside an item that has children will
* result in all children also becoming selected/unselected.
*
- Clicking on the {@link CheckBox} beside an item that has a parent will
* possibly toggle the state of the parent. For example, if you select a
* single child, the parent will become indeterminate (indicating partial
* selection of children). If you proceed to select all children, the
* parent will then show that it too is selected. This is recursive, with
* all parent nodes updating as expected.
*
*
* If it is decided that using {@link CheckBoxTreeItem} is not desirable,
* then it is necessary to call one of the constructors where a {@link Callback}
* is provided that can return an {@code ObservableValue}
* given a {@link TreeItem} instance. This {@code ObservableValue}
* should represent the boolean state of the given {@link TreeItem}.
*
* Note that the CheckBoxTreeCell renders the CheckBox 'live', meaning that
* the CheckBox is always interactive and can be directly toggled by the user.
* This means that it is not necessary that the cell enter its
* {@link #editingProperty() editing state} (usually by the user double-clicking
* on the cell). A side-effect of this is that the usual editing callbacks
* (such as {@link javafx.scene.control.TreeView#onEditCommitProperty() on edit commit})
* will not be called. If you want to be notified of changes,
* it is recommended to directly observe the boolean properties that are
* manipulated by the CheckBox.
*
* @param The type of the elements contained within the TreeView TreeItem
* instances.
* @since JavaFX 2.2
*/
public class CheckBoxTreeCell extends DefaultTreeCell {
/* *************************************************************************
* *
* Static cell factories *
* *
**************************************************************************/
/**
* Creates a cell factory for use in a TreeView control, although there is a
* major assumption when used in a TreeView: this cell factory assumes that
* the TreeView root, and all children are instances of
* {@link CheckBoxTreeItem}, rather than the default {@link TreeItem} class
* that is used normally.
*
* When used in a TreeView, the CheckBoxCell is rendered with a CheckBox
* to the right of the 'disclosure node' (i.e. the arrow). The item stored
* in {@link CheckBoxTreeItem#getValue()} will then have the StringConverter
* called on it, and this text will take all remaining horizontal space.
* Additionally, by using {@link CheckBoxTreeItem}, the TreeView will
* automatically handle situations such as:
*
*
* - Clicking on the {@link CheckBox} beside an item that has children
* will result in all children also becoming selected/unselected.
* - Clicking on the {@link CheckBox} beside an item that has a parent
* will possibly toggle the state of the parent. For example, if you
* select a single child, the parent will become indeterminate (indicating
* partial selection of children). If you proceed to select all
* children, the parent will then show that it too is selected. This is
* recursive, with all parent nodes updating as expected.
*
*
* Unfortunately, due to limitations in Java, it is necessary to provide
* an explicit cast when using this method. For example:
*
*
* {@code
* final TreeView treeView = new TreeView();
* treeView.setCellFactory(CheckBoxCell.forTreeView());}
*
* @param The type of the elements contained within the
* {@link CheckBoxTreeItem} instances.
* @return A {@link Callback} that will return a TreeCell that is able to
* work on the type of element contained within the TreeView root, and
* all of its children (recursively).
*/
public static Callback, TreeCell> forTreeView() {
Callback, ObservableValue> getSelectedProperty =
item -> {
if (item instanceof CheckBoxTreeItem>) {
return ((CheckBoxTreeItem>)item).selectedProperty();
}
return null;
};
return forTreeView(getSelectedProperty,
CellUtils.defaultTreeItemStringConverter());
}
/**
* Creates a cell factory for use in a TreeView control. Unlike
* {@link #forTreeView()}, this method does not assume that all TreeItem
* instances in the TreeView are {@link CheckBoxTreeItem} instances.
*
* When used in a TreeView, the CheckBoxCell is rendered with a CheckBox
* to the right of the 'disclosure node' (i.e. the arrow). The item stored
* in {@link CheckBoxTreeItem#getValue()} will then have the StringConverter
* called on it, and this text will take all remaining horizontal space.
*
*
Unlike {@link #forTreeView()}, this cell factory does not handle
* updating the state of parent or children TreeItems - it simply toggles
* the {@code ObservableValue} that is provided, and no more. Of
* course, this functionality can then be implemented externally by adding
* observers to the {@code ObservableValue}, and toggling the state
* of other properties as necessary.
*
* @param The type of the elements contained within the {@link TreeItem}
* instances.
* @param getSelectedProperty A {@link Callback} that, given an object of
* type {@literal TreeItem}, will return an {@code ObservableValue}
* that represents whether the given item is selected or not. This
* {@code ObservableValue} will be bound bidirectionally
* (meaning that the CheckBox in the cell will set/unset this property
* based on user interactions, and the CheckBox will reflect the state
* of the {@code ObservableValue}, if it changes externally).
* @return A {@link Callback} that will return a TreeCell that is able to
* work on the type of element contained within the TreeView root, and
* all of its children (recursively).
*/
public static Callback, TreeCell> forTreeView(
final Callback,
ObservableValue> getSelectedProperty) {
return forTreeView(getSelectedProperty, CellUtils.defaultTreeItemStringConverter());
}
/**
* Creates a cell factory for use in a TreeView control. Unlike
* {@link #forTreeView()}, this method does not assume that all TreeItem
* instances in the TreeView are {@link CheckBoxTreeItem}.
*
* When used in a TreeView, the CheckBoxCell is rendered with a CheckBox
* to the right of the 'disclosure node' (i.e. the arrow). The item stored
* in {@link TreeItem#getValue()} will then have the the StringConverter
* called on it, and this text will take all remaining horizontal space.
*
*
Unlike {@link #forTreeView()}, this cell factory does not handle
* updating the state of parent or children TreeItems - it simply toggles
* the {@code ObservableValue} that is provided, and no more. Of
* course, this functionality can then be implemented externally by adding
* observers to the {@code ObservableValue}, and toggling the state
* of other properties as necessary.
*
* @param The type of the elements contained within the {@link TreeItem}
* instances.
* @param getSelectedProperty A Callback that, given an object of
* type {@literal TreeItem}, will return an {@code ObservableValue}
* that represents whether the given item is selected or not. This
* {@code ObservableValue} will be bound bidirectionally
* (meaning that the CheckBox in the cell will set/unset this property
* based on user interactions, and the CheckBox will reflect the state of
* the {@code ObservableValue}, if it changes externally).
* @param converter A StringConverter that, give an object of type
* {@literal TreeItem}, will return a String that can be used to represent the
* object visually. The default implementation in {@link #forTreeView(Callback)}
* is to simply call .toString() on all non-null items (and to just
* return an empty string in cases where the given item is null).
* @return A {@link Callback} that will return a TreeCell that is able to
* work on the type of element contained within the TreeView root, and
* all of its children (recursively).
*/
public static Callback, TreeCell> forTreeView(
final Callback, ObservableValue> getSelectedProperty,
final StringConverter> converter) {
return tree -> new CheckBoxTreeCell<>(getSelectedProperty, converter);
}
/* *************************************************************************
* *
* Fields *
* *
**************************************************************************/
private final CheckBox checkBox;
private ObservableValue booleanProperty;
private BooleanProperty indeterminateProperty;
/* *************************************************************************
* *
* Constructors *
* *
**************************************************************************/
/**
* Creates a default {@link CheckBoxTreeCell} that assumes the TreeView is
* constructed with {@link CheckBoxTreeItem} instances, rather than the
* default {@link TreeItem}.
* By using {@link CheckBoxTreeItem}, it will internally manage the selected
* and indeterminate state of each item in the tree.
*/
public CheckBoxTreeCell() {
// getSelectedProperty as anonymous inner class to deal with situation
// where the user is using CheckBoxTreeItem instances in their tree
this(item -> {
if (item instanceof CheckBoxTreeItem>) {
return ((CheckBoxTreeItem>)item).selectedProperty();
}
return null;
});
}
/**
* Creates a {@link CheckBoxTreeCell} for use in a TreeView control via a
* cell factory. Unlike {@link CheckBoxTreeCell#CheckBoxTreeCell()}, this
* method does not assume that all TreeItem instances in the TreeView are
* {@link CheckBoxTreeItem}.
*
* To call this method, it is necessary to provide a
* {@link Callback} that, given an object of type {@literal TreeItem}, will return
* an {@code ObservableValue} that represents whether the given
* item is selected or not. This {@code ObservableValue} will be
* bound bidirectionally (meaning that the CheckBox in the cell will
* set/unset this property based on user interactions, and the CheckBox will
* reflect the state of the {@code ObservableValue}, if it changes
* externally).
*
* If the items are not {@link CheckBoxTreeItem} instances, it becomes
* the developers responsibility to handle updating the state of parent and
* children TreeItems. This means that, given a TreeItem, this class will
* simply toggles the {@code ObservableValue} that is provided, and
* no more. Of course, this functionality can then be implemented externally
* by adding observers to the {@code ObservableValue}, and toggling
* the state of other properties as necessary.
*
* @param getSelectedProperty A {@link Callback} that will return an
* {@code ObservableValue} that represents whether the given
* item is selected or not.
*/
public CheckBoxTreeCell(
final Callback, ObservableValue> getSelectedProperty) {
this(getSelectedProperty, CellUtils.defaultTreeItemStringConverter(), null);
}
/**
* Creates a {@link CheckBoxTreeCell} for use in a TreeView control via a
* cell factory. Unlike {@link CheckBoxTreeCell#CheckBoxTreeCell()}, this
* method does not assume that all TreeItem instances in the TreeView are
* {@link CheckBoxTreeItem}.
*
* To call this method, it is necessary to provide a {@link Callback}
* that, given an object of type {@literal TreeItem}, will return an
* {@code ObservableValue} that represents whether the given item
* is selected or not. This {@code ObservableValue} will be bound
* bidirectionally (meaning that the CheckBox in the cell will set/unset
* this property based on user interactions, and the CheckBox will reflect
* the state of the {@code ObservableValue}, if it changes
* externally).
*
* If the items are not {@link CheckBoxTreeItem} instances, it becomes
* the developers responsibility to handle updating the state of parent and
* children TreeItems. This means that, given a TreeItem, this class will
* simply toggles the {@code ObservableValue} that is provided, and
* no more. Of course, this functionality can then be implemented externally
* by adding observers to the {@code ObservableValue}, and toggling
* the state of other properties as necessary.
*
* @param getSelectedProperty A {@link Callback} that will return an
* {@code ObservableValue} that represents whether the given
* item is selected or not.
* @param converter {@literal A StringConverter that, give an object of type
* TreeItem, will return a String that can be used to represent the
* object visually.}
*/
public CheckBoxTreeCell(
final Callback, ObservableValue> getSelectedProperty,
final StringConverter> converter) {
this(getSelectedProperty, converter, null);
}
private CheckBoxTreeCell(
final Callback, ObservableValue> getSelectedProperty,
final StringConverter> converter,
final Callback, ObservableValue> getIndeterminateProperty) {
this.getStyleClass().add("check-box-tree-cell");
setSelectedStateCallback(getSelectedProperty);
setConverter(converter);
this.checkBox = new CheckBox();
this.checkBox.setAllowIndeterminate(false);
// by default the graphic is null until the cell stops being empty
setGraphic(null);
setAccessibleRole(AccessibleRole.CHECK_BOX_TREE_ITEM);
}
/* *************************************************************************
* *
* Properties *
* *
**************************************************************************/
// --- converter
private ObjectProperty>> converter =
new SimpleObjectProperty<>(this, "converter");
/**
* The {@link StringConverter} property.
* @return the {@link StringConverter} property
*/
public final ObjectProperty>> converterProperty() {
return converter;
}
/**
* Sets the {@link StringConverter} to be used in this cell.
* @param value the {@link StringConverter} to be used in this cell
*/
public final void setConverter(StringConverter> value) {
converterProperty().set(value);
}
/**
* Returns the {@link StringConverter} used in this cell.
* @return the {@link StringConverter} used in this cell
*/
public final StringConverter> getConverter() {
return converterProperty().get();
}
// --- selected state callback property
private ObjectProperty, ObservableValue>>
selectedStateCallback =
new SimpleObjectProperty<>(
this, "selectedStateCallback");
/**
* Property representing the {@link Callback} that is bound to by the
* CheckBox shown on screen.
* @return the property representing the {@link Callback} that is bound to
* by the CheckBox shown on screen
*/
public final ObjectProperty, ObservableValue>> selectedStateCallbackProperty() {
return selectedStateCallback;
}
/**
* Sets the {@link Callback} that is bound to by the CheckBox shown on screen.
* @param value the {@link Callback} that is bound to by the CheckBox shown on screen
*/
public final void setSelectedStateCallback(Callback, ObservableValue> value) {
selectedStateCallbackProperty().set(value);
}
/**
* Returns the {@link Callback} that is bound to by the CheckBox shown on screen.
* @return the {@link Callback} that is bound to by the CheckBox shown on screen
*/
public final Callback, ObservableValue> getSelectedStateCallback() {
return selectedStateCallbackProperty().get();
}
/* *************************************************************************
* *
* Public API *
* *
**************************************************************************/
/** {@inheritDoc} */
@Override public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
checkBox.setGraphic(null); // release the graphic so it will serve only one CheckBox
} else {
StringConverter> c = getConverter();
TreeItem treeItem = getTreeItem();
// update the node content
setText(c != null ? c.toString(treeItem) : (treeItem == null ? "" : treeItem.toString()));
checkBox.setGraphic(treeItem == null ? null : treeItem.getGraphic());
setGraphic(checkBox);
// uninstall bindings
if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
}
if (indeterminateProperty != null) {
checkBox.indeterminateProperty().unbindBidirectional(indeterminateProperty);
}
// install new bindings.
// We special case things when the TreeItem is a CheckBoxTreeItem
if (treeItem instanceof CheckBoxTreeItem) {
CheckBoxTreeItem cbti = (CheckBoxTreeItem) treeItem;
booleanProperty = cbti.selectedProperty();
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
indeterminateProperty = cbti.indeterminateProperty();
checkBox.indeterminateProperty().bindBidirectional(indeterminateProperty);
} else {
Callback, ObservableValue> callback = getSelectedStateCallback();
if (callback == null) {
throw new NullPointerException(
"The CheckBoxTreeCell selectedStateCallbackProperty can not be null");
}
booleanProperty = callback.call(treeItem);
if (booleanProperty != null) {
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
}
}
}
}
@Override void updateDisplay(T item, boolean empty) {
// no-op
// This was done to resolve RT-33603, but will impact the ability for
// TreeItem.graphic to change dynamically.
}
/** {@inheritDoc} */
@Override public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
switch (attribute) {
case TOGGLE_STATE:
if (checkBox.isIndeterminate()) {
return ToggleState.INDETERMINATE;
} else if (checkBox.isSelected()) {
return ToggleState.CHECKED;
} else {
return ToggleState.UNCHECKED;
}
default: return super.queryAccessibleAttribute(attribute, parameters);
}
}
}