3

I'm using a TreeTableView in my project, and I would like to do something specific when the user selects a row:

I would like this row to have a different background color, but i would also like its childs and parents to have this color too.

I found a way to access every rows and children, but I just don't know how to specify this background color. I tried to do this using my customs TreeTableCells and adding the style in my updateItem method, but this method is not called each time an item is selected.

So i wanted to try to add listener in my treetableview, which seems to be a better idea, but in fact i'm not able to access the rows to give them any style.

vitr
  • 6,766
  • 8
  • 30
  • 50
  • This is basically the same as http://stackoverflow.com/questions/20350099/programmatically-change-the-tableview-row-appearance/20475137#20475137 : you just need to adapt the solution there for a tree table view. – James_D Aug 22 '16 at 12:42

1 Answers1

7

The basic strategy here is:

  1. Create CSS PseudoClass instances for the conditions you want to highlight (in the example below I created one for "child of selected" and one for "parent of selected")
  2. Use a rowFactory to create rows for the table. The rows should update their pseudoclass state in the updateItem method, and should also update their pseudoclass state if the selected items change. You can place a listener on the table's selected items to do the second of these.
  3. Add CSS in the external CSS file to style the rows the way you want, using the pseudoclasses you defined in the first step.

Here is a SSCCE:

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Function;

import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener.Change;
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 TreeTableViewHighlightSelectedPath extends Application {

    private PseudoClass childOfSelected = PseudoClass.getPseudoClass("child-of-selected");
    private PseudoClass parentOfSelected = PseudoClass.getPseudoClass("parent-of-selected");

    @Override
    public void start(Stage primaryStage) {
        TreeTableView<Item> table = new TreeTableView<>(createRandomTree(50));

        table.setRowFactory(ttv -> {
            TreeTableRow<Item> row = new TreeTableRow<Item>() {
                @Override
                protected void updateItem(Item item, boolean empty) {
                    super.updateItem(item, empty);
                    if (empty) {
                        pseudoClassStateChanged(parentOfSelected, false);
                        pseudoClassStateChanged(childOfSelected, false);
                    } else {
                        updateState(this);
                    }
                }
            };

            table.getSelectionModel().getSelectedItems().addListener(
                    (Change<? extends TreeItem<Item>> c) -> updateState(row));

            return row ;
        });

        table.getColumns().add(column("Item", Item::nameProperty));
        table.getColumns().add(column("Value", Item::valueProperty));

        Scene scene = new Scene(table, 800, 800);
        scene.getStylesheets().add("table-row-highlight.css");
        primaryStage.setScene(scene);
        primaryStage.show();

    }

    private <T> void updateState(TreeTableRow<T> row) {
        TreeTableView<T> table = row.getTreeTableView() ;
        TreeItem<T> item = row.getTreeItem();

        // if item is selected, just use default "selected" highlight,
        // and set "child-of-selected" and "parent-of-selected" to false:
        if (item == null || table.getSelectionModel().getSelectedItems().contains(item)) {
            row.pseudoClassStateChanged(childOfSelected, false);
            row.pseudoClassStateChanged(parentOfSelected, false);
            return ;
        }

        // check to see if item is parent of any selected item:
        for (TreeItem<T> selectedItem : table.getSelectionModel().getSelectedItems()) {
            for (TreeItem<T> parent = selectedItem.getParent(); parent != null ; parent = parent.getParent()) {
                if (parent == item) {
                    row.pseudoClassStateChanged(parentOfSelected, true);
                    row.pseudoClassStateChanged(childOfSelected, false);
                    return ;
                }
            }
        }

        // check to see if item is child of any selected item:
        for (TreeItem<T> ancestor = item.getParent() ; ancestor != null ; ancestor = ancestor.getParent()) {
            if (table.getSelectionModel().getSelectedItems().contains(ancestor)) {
                row.pseudoClassStateChanged(childOfSelected, true);
                row.pseudoClassStateChanged(parentOfSelected, false);
                return ;
            }
        }

        // if we got this far, clear both pseudoclasses:

        row.pseudoClassStateChanged(childOfSelected, false);
        row.pseudoClassStateChanged(parentOfSelected, false);

    }

    private <S,T> TreeTableColumn<S,T> column(String title, Function<S, ObservableValue<T>> property) {
        TreeTableColumn<S,T> column = new TreeTableColumn<>(title);
        column.setCellValueFactory(cellData -> property.apply(cellData.getValue().getValue()));
        return column ;
    }

    private TreeItem<Item> createRandomTree(int numNodes) {
        Random rng = new Random();
        TreeItem<Item> root = new TreeItem<>(new Item("Item 1", rng.nextInt(1000)));
        root.setExpanded(true);
        List<TreeItem<Item>> items = new ArrayList<>();
        items.add(root);

        for (int i = 2 ; i <= numNodes; i++) {
            Item item = new Item("Item "+i, rng.nextInt(1000));
            TreeItem<Item> treeItem = new TreeItem<>(item);
            treeItem.setExpanded(true);
            items.get(rng.nextInt(items.size())).getChildren().add(treeItem);
            items.add(treeItem);
        }

        return root ;
    }

    public static class Item {
        private StringProperty name = new SimpleStringProperty();
        private IntegerProperty value = new SimpleIntegerProperty();

        public Item(String name, int value) {
            setName(name);
            setValue(value);
        }

        public final StringProperty nameProperty() {
            return this.name;
        }


        public final java.lang.String getName() {
            return this.nameProperty().get();
        }


        public final void setName(final java.lang.String name) {
            this.nameProperty().set(name);
        }


        public final IntegerProperty valueProperty() {
            return this.value;
        }


        public final int getValue() {
            return this.valueProperty().get();
        }


        public final void setValue(final int value) {
            this.valueProperty().set(value);
        }

        @Override
        public String toString() {
            return String.format("%s (%d)", getName(), getValue());
        }

    }

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

and the CSS file (table-row-highlight.css):

.tree-table-row-cell:child-of-selected {
    -fx-background: green ;
}
.tree-table-row-cell:parent-of-selected {
    -fx-background: salmon ;
}

This give the following:

enter image description here

This version highlights all descendant nodes and all ancestor nodes of the selected items in the tree. You can simplify the updateState() method if you only want immediate child and parent rows highlighted.

James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thanks a lot i'll adapt it for my code ! This is exactly the thing i wanted to do ! – William Estupina Aug 22 '16 at 13:38
  • @WilliamEstupina You're welcome. Please mark the answer as correct if it answers your question; it will make it easier for others to find it if they have a similar question. – James_D Aug 22 '16 at 13:51
  • I tried to but i just don't know how to mark it ... Do i just rename it as resolved ? – William Estupina Aug 22 '16 at 14:03
  • @WilliamEstupina Click on the check mark next to the answer. – James_D Aug 22 '16 at 14:03
  • I was able to make it works thanks to you. But only one problem is remaining: It works well with the listener on the selection changes, but i got a problem when i expand my table and then unexpand it. Some empty lines are getting colored. But when i select again another item, this stop the bug. So i would like to trigger the updateState function when ax expansion is made or unmade. Is their any easy way to get a listener on an expansion for TreeTableViews ? – William Estupina Aug 23 '16 at 07:31
  • I can't reproduce that. It happens with the example in my answer? – James_D Aug 23 '16 at 09:40