Reputation: 659
I am having a standard TreeView in JavaFX with CheckBoxTreeItem in it. I've installed a listener to see when someone checks/ unchecks a checkbox. But I want that when someone check/unchecks a checkbox I trigger that checkboxitem's parent updateItem method and change his CSS ( for example if 3 or more childs are selected for a parent then change his color to red, otherwise green).
How can I do that?
rootItem.addEventHandler(CheckBoxTreeItem.checkBoxSelectionChangedEvent(), e -> {
if (e.getTreeItem().isLeaf()) {
TreeItem<String> treeItem = (TreeItem) e.getTreeItem();
CheckBoxTreeItem<String> parentItem = (CheckBoxTreeItem<String>) treeItem.getParent();
// how to call repaint for the parentItem????
}
});
treeView.setCellFactory(p -> new CheckBoxTreeCell<>() {
@Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
// toggle the parent's CSS here
}
});
Upvotes: 3
Views: 539
Reputation: 45806
I agree with the answer by M. S. regarding the use of PseudoClass
. However, you should not be trying to manually invoke updateItem
. Instead, just add an EventHandler
to listen for "check box selection changed" events. When an event occurs in a direct child, the parent should update the pseudo-class based on (using your example) whether or not 3+ children are selected.
Here's an example which also includes a "branch" PseudoClass
so you can distinguish between a branch and a leaf in the CSS file:
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.css.PseudoClass;
import javafx.event.EventHandler;
import javafx.event.WeakEventHandler;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.control.CheckBoxTreeItem.TreeModificationEvent;
import javafx.scene.control.cell.CheckBoxTreeCell;
public class MyCheckBoxTreeCell<T> extends CheckBoxTreeCell<T> {
private static final PseudoClass BRANCH = PseudoClass.getPseudoClass("branch");
private static final PseudoClass THREE_CHILDREN_SELECTED = PseudoClass.getPseudoClass("three-children-selected");
// event handler to listen for selection changes in direct children
private final EventHandler<TreeModificationEvent<T>> handler = event -> {
/*
* Event starts from the source TreeItem and bubbles up the to the root. This means
* the first time getTreeItem() != event.getTreeItem() will be the source TreeItem's
* parent. We then consume the event to stop it propagating to the next parent.
*/
if (getTreeItem() != event.getTreeItem()) {
event.consume();
updatePseudoClasses();
}
};
private final WeakEventHandler<TreeModificationEvent<T>> weakHandler = new WeakEventHandler<>(handler);
// Used to listen for the "leaf" property of the TreeItem and update the BRANCH pseudo-class
private final InvalidationListener leafListener = observable -> updatePseudoClasses();
private final WeakInvalidationListener weakLeafListener = new WeakInvalidationListener(leafListener);
public MyCheckBoxTreeCell() {
getStyleClass().add("my-check-box-tree-cell");
// add listener to "treeItem" property to properly register and unregister
// the "leafListener" and "handler" instances.
treeItemProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.leafProperty().removeListener(weakLeafListener);
oldValue.removeEventHandler(CheckBoxTreeItem.checkBoxSelectionChangedEvent(), weakHandler);
}
if (newValue != null) {
newValue.leafProperty().addListener(weakLeafListener);
newValue.addEventHandler(CheckBoxTreeItem.checkBoxSelectionChangedEvent(), weakHandler);
}
updatePseudoClasses();
});
}
private void updatePseudoClasses() {
/*
* Assumes the use of CheckBoxTreeItem for each TreeItem in the TreeView.
*
* This code is not the most efficient as it will recalculate both the BRANCH and
* THREE_CHILDREN_SELECTED pseudo-classes each time either possibly changes.
*/
var item = (CheckBoxTreeItem<T>) getTreeItem();
if (item == null) {
pseudoClassStateChanged(BRANCH, false);
pseudoClassStateChanged(THREE_CHILDREN_SELECTED, false);
} else {
pseudoClassStateChanged(BRANCH, !item.isLeaf());
int selected = 0;
for (var child : item.getChildren()) {
// only need to know if *at least* 3 children are selected
if (((CheckBoxTreeItem<T>) child).isSelected() && ++selected >= 3) {
break;
}
}
pseudoClassStateChanged(THREE_CHILDREN_SELECTED, selected >= 3);
}
}
// No need to override "updateItem(T,boolean)" as CheckBoxTreeCell provides
// the necessary implementation which can be customized via the StringConverter
// property.
}
And then your CSS file could look like:
.my-check-box-tree-cell:branch {
-fx-background-color: green;
-fx-text-fill: white;
}
.my-check-box-tree-cell:branch:three-children-selected {
-fx-background-color: red;
-fx-text-fill: white;
}
Addressing questions in comments:
Why wrapping every listener inside a weak one if we take care to unsubscribe it?
To decrease the chance of memory leaks. For instance, if you throw away the TreeView
(without having cleared the root
property) but maintain references to the TreeItem
s somewhere, then a non-weak handler/listener would hold the TreeCell
s and the TreeView
in memory.
Why are you listening for leaf changes and when does it gets called?
To handle the case where TreeItem
s are dynamically added and/or removed. A TreeItem
is a leaf if and only if its children
list is empty. If an item is added, and the leaf now becomes a branch, we need to update the BRANCH
pseudo-class in order to have the proper CSS applied. Same if an item is removed and a branch becomes a leaf.
This may or may not be relevant to your use case. If not, then feel free to remove this part of the implementation.
You check
getTreeItem() != event.getTreeItem())
in the checkbox checked handler. Why? This will be called when a checkbox gets checked/ unchecked.
When you (un)check a CheckBoxTreeItem
it fires an event. This event begins its journey at the CheckBoxTreeItem
that was (un)checked. From there, it travels up (i.e. bubbles) the item hierarchy all the way to the root. At each item, any registered handlers will be invoked. Though if the event is consumed it does not proceed to the next parent item.
The reason we're adding the handler is to listen for any children being (un)checked—but only direct children. We don't care about changes in arbitrarily deep descendants nor the item the handler was registered to.
Since we only care about changes in direct children, we need to make sure we only react to events fired by said children. As the event is processed first by the item that was (un)checked, we need to not do anything in that first handler. We can do this by testing if the TreeItem
of the containing TreeCell
is the same one that fired the event, and the TreeModificationEvent#getTreeItem()
method returns the item that caused the event to be fired (i.e. the item that was (un)checked). If they are the same instance, do nothing and let the event bubble up to the parent.
Now the parent item's handler is processing the event. This means getTreeItem() != event.getTreeItem()
will return true
and we enter the if
block. This causes the update, if necessary, of the pseudo-classes' state. We then consume the event so it doesn't bubble up to the next parent; this effectively makes the handler only listen to events from direct children.
Note that if the parent item is not currently being displayed in the tree, then it will not be part of a cell. If its not part of a cell, it won't have had the handler added to it. Thus any non-displaying items won't be affected by any of this. This is okay since everything we're updating is purely visual; if an item isn't being displayed then there's no visuals to update.
Upvotes: 2
Reputation: 4258
You don't need to change it manually, you can use a PseudoClass:
private PseudoClass threeChildrenClass = PseudoClass.getPseudoClass("three-children");
tree.setCellFactory(param -> new CheckBoxTreeCell<String>() {
@Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setGraphic(null);
} else {
setText(item);
// Change the class based on the number of parent items
pseudoClassStateChanged(threeChildrenClass, hasThreeChildren(item));
}
}
});
In your CSS file:
.check-box-tree-cell:three-children {
-fx-background-color: red;
}
It looks like CheckBoxTreeCell doesn't have a built-in "checked" pseudo-class, you can add a "checked" PseudoClass and apply it when the tree cell is checked. Then you can call it like this:
.check-box-tree-cell:three-children:checked {
-fx-background-color: green;
}
Upvotes: 0