javafxtreetableview

JavaFX TreeTableView, Selection Bar with default color after losing focus


When customizing the cell inside the TreeTableView and then selecting any row, the color of the bar is in the default modena style, up to this point it is correct.

After losing focus the text of the selection bar is not in the default with black color

How to solve this in JavaFX, is there an elegant solution?

I tried

tableRowProperty().flatMap(TreeTableRow::focusedProperty).addListener((obs, wasSelected, isNowSelected) -> updateTextFill());

but it also does not work correctly when losing focus

enter image description here

as expected

enter image description here

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.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableRow;
import javafx.scene.control.TreeTableView; 
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Callback;

public class SelectionBarColorFocus extends Application {

    BorderPane bp = new BorderPane();
    
    @Override
    public void start(Stage primaryStage) {
         TreeTableView<Person> treeTableView = new TreeTableView<>();

        // Criar colunas
        TreeTableColumn<Person, String> nameColumn = new TreeTableColumn<>("Name");
        nameColumn.setCellValueFactory(cellData -> cellData.getValue().getValue().nameProperty());
         
        
        nameColumn.setCellFactory((TreeTableColumn<Person, String> param) -> {

            TreeTableCell<Person, String> cell = new TreeTableCell<Person, String>() {
        
                   final ImageView imv = new ImageView();
                   final Text text = new Text("");
                   
                   HBox hbox = new HBox(imv, text);
                    
                   {
                      tableRowProperty().flatMap(TreeTableRow::selectedProperty).addListener((obs, wasSelected, isNowSelected) -> updateTextFill());                       
                      // tableRowProperty().flatMap(TreeTableRow::focusedProperty).addListener((obs, wasSelected, isNowSelected) -> updateTextFill());                       
                   }
                  
                    
                    @Override
                    public void updateItem(String item, boolean empty) {
                        
                        super.updateItem(item, empty);
                        if (item != null) {
                            text.setFill(Color.ORANGE);
                            text.setText(item);
                            setGraphic(hbox);
                            
                        }else {                            
                            text.setText("");
                            setGraphic(null);
                        }
                    }
                    
                    private void updateTextFill() {
                         text.setFill(getTableRow().isSelected() ? Color.WHITE: Color.ORANGE);
                    }   
                };

                return cell;
            }
        );

        
        
        TreeTableColumn<Person, String> addressColumn = new TreeTableColumn<>("Address");
        addressColumn.setCellValueFactory(cellData -> cellData.getValue().getValue().addressProperty());



        
        TreeTableColumn<Person, Number> ageColumn = new TreeTableColumn<>("Age");
        ageColumn.setCellValueFactory(cellData -> cellData.getValue().getValue().ageProperty());
        
        ageColumn.setCellFactory((TreeTableColumn<Person, Number> param) -> {
            TreeTableCell<Person, Number> cell = new TreeTableCell<Person, Number>() {
                
                boolean flat = false;
                final Text text = new Text("");
                
                {
                  focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
                          System.out.println("jorge focused="+isNowFocused);
                      }
                  );   
                }
                
                @Override
                public void updateItem(Number item, boolean empty) {
                    super.updateItem(item, empty);
                    
                    if(flat == false) {                         
                        tableRowProperty().flatMap(TreeTableRow::selectedProperty).addListener((obs, wasSelected, isNowSelected) -> updateTextFill(item, empty));                        
                        flat = true;
                    }                    
                    updateTextFill(item, empty);
                }
                
            
                private void updateTextFill(Number item, boolean empty) {                    
                    if(item != null && item.intValue() != -1) {
                            text.setFill(Color.BLUE);                          
                            text.setText(item.toString());
                            setGraphic(text);
                        
                    }else {
                        text.setText("");
                        setText("");
                        setGraphic(null);
                    }
                    text.setFill(getTableRow().isSelected() ? Color.WHITE: Color.BLUE);
                }    
                
            };
            
            return cell;
         });
        
        nameColumn.setPrefWidth(150);
        addressColumn.setPrefWidth(200);
        ageColumn.setPrefWidth(100);
    
        treeTableView.getColumns().addAll(nameColumn, addressColumn, ageColumn);

        TreeItem<Person> rootItem = new TreeItem<>(new Person("Persons", "", -1));
        rootItem.getChildren().addAll(
                new TreeItem<>(new Person("Alice", "456 Park Ave", 25)),
                new TreeItem<>(new Person("Bob", "789 Broadway", 35)),
                new TreeItem<>(new Person("Carol", "987 Elm St", 40)),
                new TreeItem<>(new Person("John", "123 Main St", 40))
        );

        rootItem.setExpanded(true);
        treeTableView.setRoot(rootItem);
        
        
        Button b = new Button("Row Lost Focus");
        
        StackPane st = new StackPane();
        st.getChildren().add(b);
        
        bp.setCenter(treeTableView);
        bp.setBottom(st);

        Scene scene = new Scene(bp, 500, 300);
        primaryStage.setScene(scene);
        primaryStage.setTitle("JavaFX TreeTableView SelectionBar Lost Focus");
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
    
    
    
    public static class Person {
        private StringProperty name;
        private StringProperty address;
        private IntegerProperty age;

        public Person(String name, String address, int age) {
            this.name = new SimpleStringProperty(name);
            this.address = new SimpleStringProperty(address);
            this.age = new SimpleIntegerProperty(age);
        }

        public StringProperty nameProperty() {
            return name;
        }

        public StringProperty addressProperty() {
            return address;
        }

        public IntegerProperty ageProperty() {
            return age;
        }
    }
}

Solution

  • The default colors in cells (and other controls) are defined in the modena CSS file, whose source code you can see here. For cells, the text fill has the following definition:

        -fx-text-background-color: ladder(
            -fx-background,
            -fx-light-text-color 45%,
            -fx-dark-text-color  46%,
            -fx-dark-text-color  59%,
            -fx-mid-text-color   60%
        );
    

    where the individual colors are defined as

        -fx-dark-text-color: black;
        -fx-mid-text-color: #333;
        -fx-light-text-color: white;
    

    Then this is applied to text in various controls, in this case with

    .tree-table-row-cell {
        /* Other CSS properties omitted */
        -fx-text-fill: -fx-text-background-color;
    }
    

    This means that if the text is drawn over something for which fx-background is dark (less than 45% intensity), the text will be white (-fx-light-text-color); if there is a "medium" background (between 46% and 59%) the text will be black (-fx-dark-text-color), and over a light background (above 60% intensity) it will be a dark grey (-fx-mid-text-color: #333;).

    By default, the background is a very light grey, so the text appears dark grey. When the row is selected and focused, the background is the accent color, which is less than 45% intensity, so the text automatically changes to white. When the row is selected but not focussed, the background is a light grey (not as light as the unselected background) and the text becomes dark grey (the "mid text color") again.

    The recommended approach for modifying styles is to provide your own style sheet. Since you are using text objects, these are not by default styled, so you need to give them a style class (or id, but since there will be multiple text object in the same column, I recommend using a CSS class).

    From what I understand of your requirements, you want the text to be orange for the name column and blue for the age column, but only when the row is not selected. When the row is selected, you want the default colors (whether those are default for selected+focused or selected+unfocused).

    You can achieve this with the following:

    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.scene.Scene;
    import javafx.scene.control.*;
    import javafx.scene.image.ImageView;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.StackPane;
    import javafx.scene.text.Text;
    import javafx.stage.Stage;
    
    public class SelectionBarColorFocus extends Application {
    
        BorderPane bp = new BorderPane();
    
        @Override
        public void start(Stage primaryStage) {
            TreeTableView<Person> treeTableView = new TreeTableView<>();
    
            // Criar colunas
            TreeTableColumn<Person, String> nameColumn = new TreeTableColumn<>("Name");
            nameColumn.setCellValueFactory(cellData -> cellData.getValue().getValue().nameProperty());
    
    
            nameColumn.setCellFactory((TreeTableColumn<Person, String> param) -> {
    
                        TreeTableCell<Person, String> cell = new TreeTableCell<Person, String>() {
    
                            final ImageView imv = new ImageView();
                            final Text text = new Text("");
    
                            HBox hbox = new HBox(imv, text);
    
                            {
                                text.getStyleClass().add("name-cell-text");
                            }
    
    
                            @Override
                            public void updateItem(String item, boolean empty) {
    
                                super.updateItem(item, empty);
                                if (item != null) {
                                    text.setText(item);
                                    setGraphic(hbox);
    
                                }else {
                                    text.setText("");
                                    setGraphic(null);
                                }
                            }
    
                        };
    
                        return cell;
                    }
            );
    
    
    
            TreeTableColumn<Person, String> addressColumn = new TreeTableColumn<>("Address");
            addressColumn.setCellValueFactory(cellData -> cellData.getValue().getValue().addressProperty());
    
    
    
    
            TreeTableColumn<Person, Number> ageColumn = new TreeTableColumn<>("Age");
            ageColumn.setCellValueFactory(cellData -> cellData.getValue().getValue().ageProperty());
    
            ageColumn.setCellFactory((TreeTableColumn<Person, Number> param) -> {
                TreeTableCell<Person, Number> cell = new TreeTableCell<Person, Number>() {
    
                    final Text text = new Text("");
    
                    {
                        text.getStyleClass().add("age-cell-text");
                    }
    
                    @Override
                    public void updateItem(Number item, boolean empty) {
                        super.updateItem(item, empty);
                        if (item != null && item.intValue() != -1) {
                            text.setText(item.toString());
                            setGraphic(text);
                        } else {
                            setGraphic(null);
                        }
                    }
    
                };
    
                return cell;
            });
    
            nameColumn.setPrefWidth(150);
            addressColumn.setPrefWidth(200);
            ageColumn.setPrefWidth(100);
    
            treeTableView.getColumns().addAll(nameColumn, addressColumn, ageColumn);
    
            TreeItem<Person> rootItem = new TreeItem<>(new Person("Persons", "", -1));
            rootItem.getChildren().addAll(
                    new TreeItem<>(new Person("Alice", "456 Park Ave", 25)),
                    new TreeItem<>(new Person("Bob", "789 Broadway", 35)),
                    new TreeItem<>(new Person("Carol", "987 Elm St", 40)),
                    new TreeItem<>(new Person("John", "123 Main St", 40))
            );
    
            rootItem.setExpanded(true);
            treeTableView.setRoot(rootItem);
    
    
            Button b = new Button("Row Lost Focus");
    
            StackPane st = new StackPane();
            st.getChildren().add(b);
    
            bp.setCenter(treeTableView);
            bp.setBottom(st);
    
            Scene scene = new Scene(bp, 500, 300);
            scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
            primaryStage.setScene(scene);
            primaryStage.setTitle("JavaFX TreeTableView SelectionBar Lost Focus");
            primaryStage.show();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    
    
    
        public static class Person {
            private StringProperty name;
            private StringProperty address;
            private IntegerProperty age;
    
            public Person(String name, String address, int age) {
                this.name = new SimpleStringProperty(name);
                this.address = new SimpleStringProperty(address);
                this.age = new SimpleIntegerProperty(age);
            }
    
            public StringProperty nameProperty() {
                return name;
            }
    
            public StringProperty addressProperty() {
                return address;
            }
    
            public IntegerProperty ageProperty() {
                return age;
            }
        }
    }
    

    and the stylesheet

    .tree-table-row-cell .name-cell-text {
        -fx-fill: -fx-text-background-color;
        -fx-text-background-color: orange;
    }
    
    .tree-table-row-cell .age-cell-text {
        -fx-fill: -fx-text-background-color;
        -fx-text-background-color: blue;
    }
    
    .tree-table-row-cell:selected .name-cell-text, .tree-table-row-cell:selected .age-cell-text {
        /* restore default */
        -fx-text-background-color: ladder(
            -fx-background,
            -fx-light-text-color 45%,
            -fx-dark-text-color  46%,
            -fx-dark-text-color  59%,
            -fx-mid-text-color   60%
        );
    }
    

    This works by explicitly setting the -fx-text-background-color to orange or blue, and then by restoring the default ladder in a selected row. Other solutions are possible here: for example

    .tree-table-row-cell .name-cell-text {
        -fx-fill: orange;
    }
    
    .tree-table-row-cell .age-cell-text {
        -fx-fill: blue;
    }
    
    .tree-table-row-cell:selected .name-cell-text, .tree-table-row-cell:selected .age-cell-text {
        /* restore default */
        -fx-fill: -fx-text-background-color;
    }
    

    Note there are no listeners and the cell implementations are very basic.

    This can be simplified by using just the text property of the cell instead of explicit text objects. It is unclear if you need those for your real application for some unspecified reason, but here is how I would implement the cells in this specific example:

            nameColumn.setCellFactory((TreeTableColumn<Person, String> _) -> {
    
                        TreeTableCell<Person, String> cell = new TreeTableCell<>() {
                            final ImageView imv = new ImageView();
                            @Override
                            public void updateItem(String item, boolean empty) {
                                super.updateItem(item, empty);
                                if (item != null) {
                                    setText(item);
                                    setGraphic(imv);
                                } else {
                                    setText("");
                                    setGraphic(null);
                                }
                            }
    
                        };
                        cell.getStyleClass().add("name-cell");
                        return cell;
                    }
            );
    

    and

            ageColumn.setCellFactory((TreeTableColumn<Person, Number> _) -> {
                TreeTableCell<Person, Number> cell = new TreeTableCell<>() {
    
                    @Override
                    public void updateItem(Number item, boolean empty) {
                        super.updateItem(item, empty);
                        if (item != null && item.intValue() != -1) {
                            setText(item.toString());
                        } else {
                            setText("");
                        }
                    }
    
                };
                cell.getStyleClass().add("age-cell");
                return cell;
            });
    

    with the (again simpler) style sheet

    .tree-table-row-cell .name-cell {
        -fx-text-fill: orange;
    }
    .tree-table-row-cell .age-cell {
        -fx-text-fill: blue;
    }
    .tree-table-row-cell:selected .tree-table-cell {
        -fx-text-fill: inherit;
    }