Search code examples
javafxtreeviewscalafx

JavaFX TreeView -- change background CSS on sibling nodes on MouseEvent


I am trying to implement onMouseEnter and onMouseExit events on a JavaFX ListView. What I want to do is if the mouse moves over a node of the list view, I want to change the background color of the nodes that are currently visible children in the current view.

This post has a great code sample, but is not quite what I am looking for. Apply style to TreeView children nodes in javaFX

Using that code as a reference, what I am looking for is a given tree:

Root -> Item: 1 -> Item: 100 -> Item 1000, Item 1001, Item 1002, Item 1003

When I mouse over "Item: 100" I would like it and Item 1000* to have a background color change.

This seems difficult to me because the getNextSibling and getPreviousSibling interface is on the TreeItem and though you can get a TreeItem from a TreeCell on the MouseEvent, you can't (that I know of) update CSS on a TreeItem and have it take effect in a TreeCell -- because the setStyle method is on the TreeCell.

Suggestions on how this can be done?


Solution

  • [Update note: I originally had a solution using a subclass of TreeItem. The solution presented here is much cleaner than the original.]

    Create an ObservableSet<TreeItem<?>> containing the TreeItems that should be highlighted. Then in the cell factory, observe that set, and the cell's treeItemProperty(), and set the style class (I used a PseudoClass in the example below) so the cell is highlighted if the tree item belonging to the cell is in the set.

    Finally, register mouseEntered and mouseExited handlers with the cell. When the mouse enters the cell, you can get the tree item, use it to navigate to any other tree items you need, and add the appropriate items to the set you defined. In the mouseExited handler, clear the set (or perform other logic as needed).

    import java.util.HashSet;
    
    import javafx.application.Application;
    import javafx.beans.binding.Bindings;
    import javafx.beans.binding.BooleanBinding;
    import javafx.beans.value.ChangeListener;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableSet;
    import javafx.css.PseudoClass;
    import javafx.scene.Scene;
    import javafx.scene.control.TreeCell;
    import javafx.scene.control.TreeItem;
    import javafx.scene.control.TreeView;
    import javafx.scene.layout.BorderPane;
    import javafx.stage.Stage;
    
    public class HighlightingTree extends Application {
    
        private final PseudoClass highlighted = PseudoClass.getPseudoClass("highlighted");
    
        @Override
        public void start(Stage primaryStage) {
            TreeView<Integer> tree = new TreeView<>();
            tree.setRoot(buildTreeRoot());
    
            ObservableSet<TreeItem<Integer>> highlightedItems = FXCollections.observableSet(new HashSet<>());
    
    
            tree.setCellFactory(tv -> {
    
                // the cell:
                TreeCell<Integer> cell = new TreeCell<Integer>() {
    
                    // indicates whether the cell should be highlighted:
    
                    private BooleanBinding highlightCell = Bindings.createBooleanBinding(() -> 
                        getTreeItem() != null && highlightedItems.contains(getTreeItem()), 
                        treeItemProperty(), highlightedItems);
    
                    // listener for the binding above
                    // note this has to be scoped to persist alongside the cell, as the binding
                    // will use weak listeners, and we need to avoid the listener getting gc'd:
                    private ChangeListener<Boolean> listener = (obs, wasHighlighted, isHighlighted) -> 
                        pseudoClassStateChanged(highlighted, isHighlighted);
    
                    // anonymous constructor: register listener with binding    
                    {
                        highlightCell.addListener(listener);
                    }
                };
    
                // display correct text:
                cell.itemProperty().addListener((obs, oldItem, newItem) -> {
                    if (newItem == null) {
                        cell.setText(null);
                    } else {
                        cell.setText(newItem.toString());
                    }
                });
    
                // mouse listeners:
                cell.setOnMouseEntered(e -> {
                    if (cell.getTreeItem() != null) {
                        highlightedItems.add(cell.getTreeItem());
                        highlightedItems.addAll(cell.getTreeItem().getChildren());
                    }
                });
    
                cell.setOnMouseExited(e -> highlightedItems.clear());
    
                return cell ;
            });
    
            BorderPane uiRoot = new BorderPane(tree);
            Scene scene = new Scene(uiRoot, 600, 600);
            scene.getStylesheets().add("highlight-tree-children.css");
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        private TreeItem<Integer> buildTreeRoot() {
            return buildTreeItem(1);
        }
    
        private TreeItem<Integer> buildTreeItem(int n) {
            TreeItem<Integer> item = new TreeItem<>(n);
            if (n < 10_000) {
                for (int i = 0; i<10; i++) {
                    item.getChildren().add(buildTreeItem(n * 10 + i));
                }
            }
            return item ;
        }
    
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    highlight-tree-children.css:

    .tree-cell:highlighted {
        -fx-background: yellow ;
    }