Murray
Murray

Reputation: 313

JavaFX: Adding CTRL-click functionality to checkboxes of nested CheckBoxTreeItem[s]

I have a situation where a TreeView is being displayed, with two levels of entries (parents and children), like so:

root (invisible)
|_ parent item 1
   |_ child item 1-1
   |_ child item 1-2
|_ parent item 2
   |_ child item 2-1

These items are all standard CheckBoxTreeItems. What I want to do, is to have CTRL-clicking on a parent item's checkbox select a set of it's children, according to some function. For example, here I might want only the first child item (i.e. child item 1-1 and child item 2-1) in each child list to be selected upon CTRL-clicking the parent checkbox.

Is this possible? As far as I can see, there's no good way to access the checkbox and give it e.g. an onMouseClick event handler, which is the solution that would make sense to me.

The code for the example tree layout given above:

TreeViewTest.java

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.cell.CheckBoxTreeCell;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class TreeViewTest extends Application {

    @Override
    public void start(final Stage stage) {
        StackPane sceneRoot = new StackPane();

        // create the tree model
        CheckBoxTreeItem<String> parent1 = new CheckBoxTreeItem<>("parent 1");
        CheckBoxTreeItem<String> parent2 = new CheckBoxTreeItem<>("parent 2");
        CheckBoxTreeItem<String> child1_1 = new CheckBoxTreeItem<>("child 1-1");
        CheckBoxTreeItem<String> child1_2 = new CheckBoxTreeItem<>("child 1-2");
        CheckBoxTreeItem<String> child2_1 = new CheckBoxTreeItem<>("child 2-1");
        CheckBoxTreeItem<String> root = new CheckBoxTreeItem<>("root");

        // attach the nodes
        parent1.getChildren().addAll(child1_1, child1_2);
        parent2.getChildren().addAll(child2_1);
        root.getChildren().addAll(parent1, parent2);

        // display everything
        root.setExpanded(true);
        parent1.setExpanded(true);
        parent2.setExpanded(true);

        // create the treeView
        final TreeView<String> treeView = new TreeView<>();
        treeView.setShowRoot(false);
        treeView.setRoot(root);

        // set the cell factory
        treeView.setCellFactory(CheckBoxTreeCell.forTreeView());

        // display the tree
        sceneRoot.getChildren().addAll(treeView);
        Scene scene = new Scene(sceneRoot, 200, 200);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        Main.launch(args);
    }
}

Upvotes: 1

Views: 418

Answers (2)

Murray
Murray

Reputation: 313

As suggested by @James_D in the comments on the question, one solution is to create a custom TreeCell implementation which exposes the CheckBox.

The modified TreeViewTest.java

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Callback;

public class TreeViewTest extends Application {

    @Override
    public void start(final Stage stage) {
        StackPane sceneRoot = new StackPane();

        // create the tree model
        CheckBoxTreeItem<String> parent1 = new CheckBoxTreeItem<>("parent 1");
        CheckBoxTreeItem<String> parent2 = new CheckBoxTreeItem<>("parent 2");
        CheckBoxTreeItem<String> child1_1 = new CheckBoxTreeItem<>("child 1-1");
        CheckBoxTreeItem<String> child1_2 = new CheckBoxTreeItem<>("child 1-2");
        CheckBoxTreeItem<String> child2_1 = new CheckBoxTreeItem<>("child 2-1");
        CheckBoxTreeItem<String> root = new CheckBoxTreeItem<>("root");

        // attach the nodes
        parent1.getChildren().addAll(child1_1, child1_2);
        parent2.getChildren().addAll(child2_1);
        root.getChildren().addAll(parent1, parent2);

        // display everything
        root.setExpanded(true);
        parent1.setExpanded(true);
        parent2.setExpanded(true);

        // create the treeView
        final TreeView<String> treeView = new TreeView<>();
        treeView.setShowRoot(false);
        treeView.setRoot(root);

        // set the cell factory UPDATED
        treeView.setCellFactory(new Callback<TreeView<String>,TreeCell<String>>() {

            @Override
            public TreeCell<String> call(TreeView<String> param) {
                TreeCell<String> cell = new MyTreeCell<>();

                ((MyTreeCell) cell).getCheckBox().setOnMouseClicked(e -> {
                    if (!cell.getTreeItem().isLeaf())
                        if (e.isControlDown() && e.getButton() == MouseButton.PRIMARY)
                            System.out.println("CTRL-clicked");
                });

                return cell;
            }
        });

        // display the tree
        sceneRoot.getChildren().addAll(treeView);
        Scene scene = new Scene(sceneRoot, 200, 200);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        Main.launch(args);
    }
}

MyTreeCell.java (slightly tweaked code copied from CheckBoxTreeCell)

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.CheckBoxTreeItem;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.util.Callback;
import javafx.util.StringConverter;

public class MyTreeCell<T> extends TreeCell<T> {
    private final CheckBox checkBox;
    private ObservableValue<Boolean> booleanProperty;
    private BooleanProperty indeterminateProperty;

    public MyTreeCell() {
        this.getStyleClass().add("check-box-tree-cell");
        this.checkBox = new CheckBox();
        this.checkBox.setAllowIndeterminate(false);

        // by default the graphic is null until the cell stops being empty
        setGraphic(null);
    }

    // --- checkbox
    public final CheckBox getCheckBox() { return checkBox; }

    // --- selected state callback property
    private ObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>>
            selectedStateCallback =
            new SimpleObjectProperty<>(
                    this, "selectedStateCallback");

    public final ObjectProperty<Callback<TreeItem<T>, ObservableValue<Boolean>>> selectedStateCallbackProperty() {
        return selectedStateCallback;
    }

    public final void setSelectedStateCallback(Callback<TreeItem<T>, ObservableValue<Boolean>> value) {
        selectedStateCallbackProperty().set(value);
    }

    public final Callback<TreeItem<T>, ObservableValue<Boolean>> getSelectedStateCallback() {
        return selectedStateCallbackProperty().get();
    }

    @Override public void updateItem(T item, boolean empty) {
        super.updateItem(item, empty);

        if (empty) {
            setText(null);
            setGraphic(null);
        } else {    
            TreeItem<T> treeItem = getTreeItem();

            // update the node content
            setText((treeItem == null ? "" : treeItem.getValue().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<T> cbti = (CheckBoxTreeItem<T>) treeItem;
                booleanProperty = cbti.selectedProperty();
                checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);

                indeterminateProperty = cbti.indeterminateProperty();
                checkBox.indeterminateProperty().bindBidirectional(indeterminateProperty);
            } else {
                Callback<TreeItem<T>, ObservableValue<Boolean>> 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);
                }
            }
        }
    }
}

Upvotes: 0

James_D
James_D

Reputation: 209358

You need a custom implementation of TreeCell. This should give you a starting point that allows you to implement the additional functionality you need:

public class MyCheckBoxCell extends TreeCell<String> {

    private final CheckBox checkBox = new CheckBox();

    private BooleanProperty currentSelectedBinding ;

    // only need this if you are using the indeterminateProperty() of your
    // CheckBoxTreeItems
    private BooleanProperty currentIndeterminateBinding ;

    public MyCheckBoxCell() {

        // add extra event handling to the check box here...

    }

    @Override
    protected void updateItem(String item, boolean empty) {

        super.updateItem(item, empty);

        if (empty) {
            setText(null);
            setGraphic(null);
        } else {
            setText(item);
            setGraphic(checkBox);
            if (currentSelectedBinding != null) {
                checkBox.selectedProperty().unbindBidirectional(currentSelectedBinding);
            }
            if (currentIndeterminateBinding != null) {
                checkBox.indeterminateProperty().unbindBidirectional(currentIndeterminateBinding);
            }
            if (getTreeItem() instanceof CheckBoxTreeItem) {
                CheckBoxTreeItem cbti = (CheckBoxTreeItem<?>) getTreeItem();
                currentSelectedBinding = cbti.selectedProperty();
                checkBox.selectedProperty().bindBidirectional(currentSelectedBinding);
                currentIndeterminateBinding = cbti.indeterminateProperty();
                checkBox.indeterminateProperty().bindBidirectional(currentIndeterminateBinding);
            }
        }
    }
}

Upvotes: 1

Related Questions