Search code examples
javajavafxjavafx-8selectiontreetableview

Also select parents up to the root when selecting child in TreeTableView


We try to achieve the following:

When a node gets selected in a JavaFX TreeTableView, also "the path to the root", i.e., the parent, the grandparent, and so on should get selected. Selected in this case means highlighted with a different background color, see the image (in the example, the node on Level 2 has been clicked by the user).

enter image description here

Is there a built-in function how to achieve this? We tried using CSS but did not succeed.


Solution

  • There's no "built-in function" to do this. Use a row factory on the tree table view to create rows that observe the selected item, and set a pseudoclass on the row accordingly.

    For example:

    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;
    
    import javafx.application.Application;
    import javafx.beans.property.IntegerProperty;
    import javafx.beans.property.SimpleIntegerProperty;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.css.PseudoClass;
    import javafx.scene.Scene;
    import javafx.scene.control.TreeItem;
    import javafx.scene.control.TreeTableColumn;
    import javafx.scene.control.TreeTableRow;
    import javafx.scene.control.TreeTableView;
    import javafx.stage.Stage;
    
    public class TreeTableViewHighlightSelectionPath extends Application {
    
        @Override
        public void start(Stage primaryStage) {
            TreeTableView<Item> table = new TreeTableView<Item>();
    
            PseudoClass ancestorOfSelection = PseudoClass.getPseudoClass("ancestor-of-selection");
    
            table.setRowFactory(ttv -> new TreeTableRow<Item>() {
    
                {
                    table.getSelectionModel().selectedItemProperty().addListener(
                            (obs, oldSelection, newSelection) -> updateStyleClass());
                }
                @Override
                protected void updateItem(Item item, boolean empty) {
                    super.updateItem(item, empty);
                    updateStyleClass();
                }
    
                private void updateStyleClass() {
                    pseudoClassStateChanged(ancestorOfSelection, false);
                    TreeItem<Item> treeItem = table.getSelectionModel().getSelectedItem();
                    if (treeItem != null) {
                        for (TreeItem<Item> parent = treeItem.getParent() ; parent != null ; parent = parent.getParent()) {
                            if (parent == getTreeItem()) {
                                pseudoClassStateChanged(ancestorOfSelection, true);
                                break ;
                            }
                        }
                    }
                }
            });
    
            TreeTableColumn<Item, String> itemCol = new TreeTableColumn<>("Item");
            itemCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getValue().getName()));
            table.getColumns().add(itemCol);
    
            TreeTableColumn<Item, Number> valueCol = new TreeTableColumn<>("Value");
            valueCol.setCellValueFactory(cellData -> cellData.getValue().getValue().valueProperty());
            table.getColumns().add(valueCol);
    
            table.setRoot(createRandomTree());
    
            Scene scene = new Scene(table);
            scene.getStylesheets().add("style.css");
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        private TreeItem<Item> createRandomTree() {
            TreeItem<Item> root = new TreeItem<>(new Item("Item 1", 0));
            Random rng = new Random();
            List<TreeItem<Item>> items = new ArrayList<>();
            items.add(root);
    
            for (int i = 2 ; i <= 20 ; i++) {
                TreeItem<Item> item = new TreeItem<>(new Item("Item "+i, rng.nextInt(1000)));
                items.get(rng.nextInt(items.size())).getChildren().add(item);
                items.add(item);
            }
    
            return root ;
        }
    
        public static class Item {
            private final String name ;
            private final IntegerProperty value = new SimpleIntegerProperty();
    
            public Item(String name, int value) {
                this.name = name ;
                setValue(value);
            }
    
            public String getName() {
                return name ;
            }
    
            public IntegerProperty valueProperty() {
                return value ;
            }
    
            public final int getValue() {
                return valueProperty().get();
            }
    
            public final void setValue(int value) {
                valueProperty().set(value);
            }
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    Now you can just style the "ancestor of a selected node" in CSS:

    File style.css:

    .tree-table-row-cell:ancestor-of-selection {
        -fx-background: -fx-selection-bar;
        -fx-table-cell-border-color: derive(-fx-selection-bar, 20%);
    }
    

    (You may want to modify the CSS to get better control, e.g. set different colors for selected rows in a non-focused table, etc. See the default stylesheet for details on the default style.)

    Here's a screenshot of the above test app:

    enter image description here