My goal is to add a feature to the existing TreeTableView
where clicking on the disclosure node of a group with Alt key held down should only expand the associated group, but with all its children (subgroups) collapsed regardless if they were expanded beforehand.
Without the Alt key being held down, the disclosure node should just trigger the "default" behavior.
For example, let's assume user has the following hierarchy of nodes:
ROOT
-> GROUP_1
----> GROUP_1_1
--------> CHILD_1_1_1
--------> CHILD_1_1_2
--------> CHILD_1_1_3
----> GROUP_1_2
--------> CHILD_1_2_1
--------> CHILD_1_2_2
-> GROUP_2
-> GROUP_3
-> ...
When user holds down the Alt key and clicks on the disclosure node next to ROOT
to expand it, system is expected to show. It's essential that groups GROUP_1_1
and GROUP_1_2
are collapsed.
ROOT
-> GROUP_1
-> GROUP_2
-> GROUP_3
-> ...
The difference between this behavior and the default one is that if user does holds down the Alt key and clicks on the disclosure node next to ROOT
then system must make sure everything inside is collapsed in the end.
I am looking around functions like CellBehaviorBase#doSelect
and TreeTableCellBehavior#handleDisclosureNode
, but so far I have not been able to find any hint how to achieve what I want.
Minimal example for my tests. The aim is to have a possibility to expand the root group ("Cars") with all subgroups collapsed when ALT
is held down:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class HelloApplication extends Application {
@Override
public void start(Stage stage) {
Pane rootNode = new VBox();
Scene scene = new Scene(rootNode, 400, 300);
stage.setTitle("TreeTableView Demo");
stage.setScene(scene);
TreeTableView<Car> treeTableView = new TreeTableView<>();
TreeTableColumn<Car, String> treeTableColumn1 = new TreeTableColumn<>("Brand");
treeTableColumn1.setPrefWidth(120);
TreeTableColumn<Car, String> treeTableColumn2 = new TreeTableColumn<>("Model");
treeTableColumn2.setPrefWidth(120);
treeTableColumn1.setCellValueFactory(param -> param.getValue().getValue().brandProperty());
treeTableColumn2.setCellValueFactory(param -> param.getValue().getValue().modelProperty());
treeTableView.getColumns().add(treeTableColumn1);
treeTableView.getColumns().add(treeTableColumn2);
TreeItem<Car> mercedes1 = new TreeItem<>(new Car("Mercedes", "SL500"));
TreeItem<Car> mercedes2 = new TreeItem<>(new Car("Mercedes", "SL500 AMG"));
TreeItem<Car> mercedes3 = new TreeItem<>(new Car("Mercedes", "CLA 200"));
TreeItem<Car> mercedes = new TreeItem<>(new Car("Mercedes", "..."));
mercedes.getChildren().add(mercedes1);
mercedes.getChildren().add(mercedes2);
TreeItem<Car> audi1 = new TreeItem<>(new Car("Audi", "A1"));
TreeItem<Car> audi2 = new TreeItem<>(new Car("Audi", "A5"));
TreeItem<Car> audi3 = new TreeItem<>(new Car("Audi", "A7"));
TreeItem<Car> audi = new TreeItem<>(new Car("Audi", "..."));
audi.getChildren().add(audi1);
audi.getChildren().add(audi2);
audi.getChildren().add(audi3);
TreeItem<Car> cars = new TreeItem<>(new Car("Cars", "..."));
cars.getChildren().add(audi);
cars.getChildren().add(mercedes);
treeTableView.setRoot(cars);
rootNode.getChildren().add(treeTableView);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
class Car {
private StringProperty brand = new SimpleStringProperty();
private StringProperty model = new SimpleStringProperty();
public Car(String brand, String model) {
this.brand.set(brand);
this.model.set(model);
}
public String getBrand() {
return brand.get();
}
public StringProperty brandProperty() {
return brand;
}
public void setBrand(String brand) {
this.brand.set(brand);
}
public String getModel() {
return model.get();
}
public StringProperty modelProperty() {
return model;
}
public void setModel(String model) {
this.model.set(model);
}
}
Your idea of dealing with Behavior
logic is valid. But unfortunately that does not help. As the cell behavior is tightly bounded within the cell skin, it will be very hard to control the logic that happens internally before you try to take the control (even with custom Cell, Skin & Behavior implementation).
Having said that, you can look for other ways to tweak this. And one way I can think of is to implement a custom event dispatcher and divert the event processing when your desired conditions are met.
The general idea of this approach is :
Below is the code of the custom cell as per above approach.
class CustomTreeTableCell<T, S> extends TreeTableCell<T, S> {
public CustomTreeTableCell() {
// Keep the reference of original dispatcher
final EventDispatcher orig = getEventDispatcher();
// Set a new event dispatcher
setEventDispatcher((event, tail) -> {
// If the event is mouse pressed and is alt down, then do this logic otherwise proceed with usual orig dispatch.
if (event instanceof MouseEvent me && me.getEventType() == MouseEvent.MOUSE_PRESSED && me.isAltDown()) {
Node disclosureNode = getTableRow().getDisclosureNode();
// Check if disclosure node exists and visible
if (disclosureNode != null && disclosureNode.isVisible()) {
// Check if the mouse press happened on the node by checking the mouse point on node bounds.
if (disclosureNode.localToScene(disclosureNode.getLayoutBounds()).contains(me.getSceneX(), me.getSceneY())) {
// When you reach this point, you do your logic. ie. collapse all its child items and expand this item.
final TreeItem<T> treeItem = getTableRow().getTreeItem();
treeItem.getChildren().forEach(this::collapse);
treeItem.setExpanded(true);
// Ensure to consume the event by returning null;
return null;
}
}
}
// If it is not your desired event, let the original dispatcher do its stuff.
return orig.dispatchEvent(event, tail);
});
}
/**
* Recursive method to collapse all child items and provided item.
*
* @param item tree item
*/
private void collapse(TreeItem<?> item) {
item.setExpanded(false);
item.getChildren().forEach(this::collapse);
}
@Override
protected void updateItem(S item, boolean empty) {
super.updateItem(item, empty);
setText(empty || item == null ? "" : item.toString());
}
}
Note: the approach of checking the mouse pointer on the disclosure node bounds is not something new. The internal logic of TreeTableCellBehavior
also does that in handleDisclosureNode
method.
Below is the full demo with your example:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.EventDispatcher;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TreeTableView_Demo extends Application {
@Override
public void start(Stage stage) {
Pane rootNode = new VBox();
Scene scene = new Scene(rootNode, 400, 300);
stage.setTitle("TreeTableView Demo");
stage.setScene(scene);
TreeTableView<Car> treeTableView = new TreeTableView<>();
TreeTableColumn<Car, String> treeTableColumn1 = new TreeTableColumn<>("Brand");
treeTableColumn1.setCellFactory(p -> new CustomTreeTableCell<>());
treeTableColumn1.setPrefWidth(120);
TreeTableColumn<Car, String> treeTableColumn2 = new TreeTableColumn<>("Model");
treeTableColumn2.setPrefWidth(120);
treeTableColumn1.setCellValueFactory(param -> param.getValue().getValue().brandProperty());
treeTableColumn2.setCellValueFactory(param -> param.getValue().getValue().modelProperty());
treeTableView.getColumns().add(treeTableColumn1);
treeTableView.getColumns().add(treeTableColumn2);
TreeItem<Car> mercedes1 = new TreeItem<>(new Car("Mercedes", "SL500"));
TreeItem<Car> mercedes2 = new TreeItem<>(new Car("Mercedes", "SL500 AMG"));
TreeItem<Car> mercedes3 = new TreeItem<>(new Car("Mercedes", "CLA 200"));
TreeItem<Car> mercedes = new TreeItem<>(new Car("Mercedes", "..."));
mercedes.getChildren().add(mercedes1);
mercedes.getChildren().add(mercedes2);
TreeItem<Car> audi1 = new TreeItem<>(new Car("Audi", "A1"));
TreeItem<Car> audi2 = new TreeItem<>(new Car("Audi", "A5"));
TreeItem<Car> audi3 = new TreeItem<>(new Car("Audi", "A7"));
TreeItem<Car> audi = new TreeItem<>(new Car("Audi", "..."));
audi.getChildren().add(audi1);
audi.getChildren().add(audi2);
audi.getChildren().add(audi3);
TreeItem<Car> cars = new TreeItem<>(new Car("Cars", "..."));
cars.getChildren().add(audi);
cars.getChildren().add(mercedes);
treeTableView.setRoot(cars);
rootNode.getChildren().add(treeTableView);
stage.show();
}
public static void main(String[] args) {
launch();
}
class CustomTreeTableCell<T, S> extends TreeTableCell<T, S> {
public CustomTreeTableCell() {
// Keep the reference of original dispatcher
final EventDispatcher orig = getEventDispatcher();
// Set a new event dispatcher
setEventDispatcher((event, tail) -> {
// If the event is mouse pressed and is alt down, then do this logic otherwise proceed with usual orig dispatch.
if (event instanceof MouseEvent me && me.getEventType() == MouseEvent.MOUSE_PRESSED && me.isAltDown()) {
Node disclosureNode = getTableRow().getDisclosureNode();
// Check if disclosure node exists and visible
if (disclosureNode != null && disclosureNode.isVisible()) {
// Check if the mouse press happened on the node by checking the mouse point on node bounds.
if (disclosureNode.localToScene(disclosureNode.getLayoutBounds()).contains(me.getSceneX(), me.getSceneY())) {
// When you reach this point, you do your logic. ie. collapse all its child items and expand this item.
final TreeItem<T> treeItem = getTableRow().getTreeItem();
treeItem.getChildren().forEach(this::collapse);
treeItem.setExpanded(true);
// Ensure to consume the event by returning null;
return null;
}
}
}
// If it is not your desired event, let the original dispatcher do its stuff.
return orig.dispatchEvent(event, tail);
});
}
/**
* Recursive method to collapse all child items and provided item.
*
* @param item tree item
*/
private void collapse(TreeItem<?> item) {
item.setExpanded(false);
item.getChildren().forEach(this::collapse);
}
@Override
protected void updateItem(S item, boolean empty) {
super.updateItem(item, empty);
setText(empty || item == null ? "" : item.toString());
}
}
}
class Car {
private StringProperty brand = new SimpleStringProperty();
private StringProperty model = new SimpleStringProperty();
public Car(String brand, String model) {
this.brand.set(brand);
this.model.set(model);
}
public String getBrand() {
return brand.get();
}
public StringProperty brandProperty() {
return brand;
}
public void setBrand(String brand) {
this.brand.set(brand);
}
public String getModel() {
return model.get();
}
public StringProperty modelProperty() {
return model;
}
public void setModel(String model) {
this.model.set(model);
}
}