JavaFx : How to make a persistent selection with CSS in a TableView

Issue

Update: inserted project (reproductible example) and last details

I have an application that is displaying a TableView filled with simple objects (observable list)

I want to display selected items (rows) in a TableView highlighting them.

Ex: If the user press ‘Insert‘ i update (in the observable list) the object which is selected. A boolean in the object will do. The objects are ‘marked’; the user can do something else.

I cannot use myTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); as the user will loose the selection as soon as a key is pressed or a mouse clik happens.

With that in mind, this means i’m managing keyboard like so:


    public boolean implementListenerPackage(Scene s) {
          //some init then...
          s.setOnKeyReleased(new EventHandler<KeyEvent>() {
                @Override
                public void handle(KeyEvent ke) {
                    switch (ke.getCode()) {
                        case INSERT:
                            setObservableListObjectSelect();
                            break;
                   }
               }
        });
    }

The object in the observable list is rather simple:

public class myObject {
    private boolean selected;
    private String otherStuff = "";
// Then constructor , getters and setters

And i have a MouseEvent management to handle other actions as well. When i’m creating my TableView i add this:

                myTableView.setRowFactory(rftv-> {
                    TableRow<type> rowObj = new TableRow<>();
                    rowObj.setOnMousePressed(new EventHandler<MouseEvent>() {
                        @Override
                        public void handle(MouseEvent e) {
                           if (e.getClickCount() == 2 && (!rowObj.isEmpty())) {
                                SomeClass.doSomethingForDoubleClik()
                            } else { // Simple clic
                                SomeClass.doSomethingForSimpleClik()
                            }
                        }
                    });
                    return rowObj;
                });

My goal is to change the CSS of a row when the myObject boolean changes. And by doing so make the user selection highlighted even though the user click on another row.

I tried :

  • A lot of reading on this website, but only to find simple examples. Not the one i have with several obstacles at the same time.
  • to implement more things in the rowFactory. But i couldn’t do it. If it compiles, then it crashes with a nullpointerexception. I never been good at those tricky syntax things.
  • to do it directly from the keyboard management but only to find it’s rather complicated. I have to get the selected object, update it, then find the selected cells and change the CSS one by one (column logic).
  • to implement a "binding" (An example here) between the object and the row but only to find myself wondering how to implement it as it was an answer for a different problem ‘flavor’.

It’s probably in front of my eyes but i can’t see it.


update:
Bear in mind that

  • the keyboard management is centralized.
  • there is already a factory set on the tableView.
  • They are several TableView in the original application so i whish the CSS style change automatically and not in some kind of ‘hardcoded’ fashion.

The minimal code:

package application;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;

public class Main extends Application {

    Label lbl01 = new Label("Information");

    @Override
    public void start(Stage primaryStage) {
        try {
            TableView tv1 = new TableView();
            TableColumn<MyObject, String> column1 = new TableColumn<>("Col 01");
            column1.setCellValueFactory(new PropertyValueFactory<>("keyboardSelected"));

            TableColumn<MyObject, String> column2 = new TableColumn<>("Col 02");
            column2.setCellValueFactory(new PropertyValueFactory<>("dataA"));

            TableColumn<MyObject, String> column3 = new TableColumn<>("Col 03");
            column3.setCellValueFactory(new PropertyValueFactory<>("dataB"));

            TableColumn<MyObject, String> column4 = new TableColumn<>("Col 04");
            column4.setCellValueFactory(new PropertyValueFactory<>("dataC"));

            tv1.getColumns().add(column1);
            tv1.getColumns().add(column2);
            tv1.getColumns().add(column3);
            tv1.getColumns().add(column4);

            ObservableList<MyObject> olm1 = FXCollections.observableArrayList();
            olm1.addAll(new MyObject(false, "Object01 A", "Object01 B", "Object01 C"),
                    new MyObject(false, "Object02 A", "Object02 B", "Object02 C"),
                    new MyObject(false, "Object03 A", "Object03 B", "Object03 C"),
                    new MyObject(false, "Object04 A", "Object04 B", "Object04 C"),
                    new MyObject(false, "Object05 A", "Object05 B", "Object05 C")
                    );

            tv1.setItems(olm1);

            tv1.setRowFactory(dc -> {
                TableRow<MyObject> rowObj = new TableRow<>();
                rowObj.setOnMousePressed(new EventHandler<MouseEvent>() {
                    @Override
                    public void handle(MouseEvent e) {
                        if (e.getClickCount() == 2 && (!rowObj.isEmpty())) {
                            lbl01.setText("Double click on line " + tv1.getSelectionModel().getSelectedIndex());

                        } else {
                            lbl01.setText("Single click on line " + +tv1.getSelectionModel().getSelectedIndex());
                        }
                    }
                });
                return rowObj;
            });

            VBox root = new VBox(tv1, lbl01);
            Scene scene = new Scene(root, 512, 640);
            primaryStage.setScene(scene);

            KeyboardManagement km = new KeyboardManagement();
            km.implementListener(scene, lbl01, tv1);

            primaryStage.show();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

}


package application;

import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;

public class KeyboardManagement {
    public KeyboardManagement() {
    }

    public boolean implementListener(Scene s, Label l, TableView tv1) {

        boolean retRep = false;
        try {
            s.setOnKeyReleased(new EventHandler<KeyEvent>() {
                @Override
                public void handle(KeyEvent ke) {
                    if (ke.getCode() == KeyCode.SPACE) {
                        MyObject m1 = (MyObject) tv1.getSelectionModel().getSelectedItem();
                        tv1.refresh();
                        m1.setKeyboardSelected(!m1.isKeyboardSelected());
                        l.setText("Space was pressed / TableView Line :" + tv1.getSelectionModel().getSelectedIndex()
                                + ". " + m1.toString());
                    }

                }
            });
        } catch (Exception e) {

        }
        return retRep;
    }

}
package application;


public class MyObject {

    private boolean keyboardSelected;
    private String dataA;
    private String dataB;
    private String dataC;

    public MyObject(boolean keyboardSelected, String dataA, String dataB, String dataC) {
            super();
            this.keyboardSelected = keyboardSelected;
            this.dataA = dataA;
            this.dataB = dataB;
            this.dataC = dataC;
        }

    public boolean isKeyboardSelected() {
        return keyboardSelected;
    }

    public void setKeyboardSelected(boolean keyboardSelected) {
        this.keyboardSelected = keyboardSelected;
    }

    public String getDataA() {
        return dataA;
    }

    public void setDataA(String dataA) {
        this.dataA = dataA;
    }

    public String getDataB() {
        return dataB;
    }

    public void setDataB(String dataB) {
        this.dataB = dataB;
    }

    public String getDataC() {
        return dataC;
    }

    public void setDataC(String dataC) {
        this.dataC = dataC;
    }

    @Override
    public String toString() {
        return "MyObject [keyboardSelected=" + keyboardSelected + ", dataA=" + dataA + ", dataB=" + dataB + ", dataC="
                + dataC + "]";
    }


}

Solution

Found it!

Simple and complex at the same time.

The trick is to insert at the right place a listener on the object property.

  • Inside the rowfactory definition.
  • Before the @override
  • Pass the row object as an argument and ‘voila’.

Something like :

rowObj.itemProperty().addListener((observable, oldValue, newValue) -> updateTableRowCss(rowObj, newValue));

Notes:

  • You can add several listeners on different object properties.

  • I strongly suggest you create a method the listener will call. Sometimes the IDE syntax analyzers are making bloodshed in the screen. It doesn’t help finding errors in this type of code.

  • From the tests i made, it seems that the modifications on the style you do will preval (like more important) on the already loaded style. Which is fine.


This way you keep a centralized keyboard management (like in this example) and the mouse event management in the factory.

The cursor still moves freely and you keep an object selection you can reuse later.

The modified example code from above:


    package application;
    
    import javafx.application.Application;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.event.EventHandler;
    import javafx.stage.Stage;
    import javafx.scene.Scene;
    import javafx.scene.control.Label;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableRow;
    import javafx.scene.control.TableView;
    import javafx.scene.control.cell.PropertyValueFactory;
    import javafx.scene.input.MouseEvent;
    import javafx.scene.layout.VBox;
    
    public class Main extends Application {
    
        Label lbl01 = new Label("Information");
    
        @Override
        public void start(Stage primaryStage) {
            try {
                TableView tv1 = new TableView();
                TableColumn<MyObject, String> column1 = new TableColumn<>("Col 01");
                column1.setCellValueFactory(new PropertyValueFactory<>("keyboardSelected"));
    
                TableColumn<MyObject, String> column2 = new TableColumn<>("Col 02");
                column2.setCellValueFactory(new PropertyValueFactory<>("dataA"));
    
                TableColumn<MyObject, String> column3 = new TableColumn<>("Col 03");
                column3.setCellValueFactory(new PropertyValueFactory<>("dataB"));
    
                TableColumn<MyObject, String> column4 = new TableColumn<>("Col 04");
                column4.setCellValueFactory(new PropertyValueFactory<>("dataC"));
    
                tv1.getColumns().add(column1);
                tv1.getColumns().add(column2);
                tv1.getColumns().add(column3);
                tv1.getColumns().add(column4);
    
                ObservableList<MyObject> olm1 = FXCollections.observableArrayList();
                olm1.addAll(new MyObject(false, "Object01 A", "Object01 B", "Object01 C"),
                        new MyObject(false, "Object02 A", "Object02 B", "Object02 C"),
                        new MyObject(false, "Object03 A", "Object03 B", "Object03 C"),
                        new MyObject(false, "Object04 A", "Object04 B", "Object04 C"),
                        new MyObject(false, "Object05 A", "Object05 B", "Object05 C")
                        );
    
                tv1.setItems(olm1);
    
                tv1.setRowFactory(dc -> {
                    TableRow<MyObject> rowObj = new TableRow<>();
                    rowObj.itemProperty().addListener((observable, oldValue, newValue) -> updateTableRowCss(rowObj, newValue));
                    rowObj.setOnMousePressed(new EventHandler<MouseEvent>() {
                        @Override
                        public void handle(MouseEvent e) {
                            if (e.getClickCount() == 2 && (!rowObj.isEmpty())) {
                                lbl01.setText("Double click on line " + tv1.getSelectionModel().getSelectedIndex());
    
                            } else {
                                lbl01.setText("Single click on line " + +tv1.getSelectionModel().getSelectedIndex());
                            }
                        }
                    });
                    return rowObj;
                });
    
                VBox root = new VBox(tv1, lbl01);
                Scene scene = new Scene(root, 512, 640);
                primaryStage.setScene(scene);
    
                KeyboardManagement km = new KeyboardManagement();
                km.implementListener(scene, lbl01, tv1);
    
                primaryStage.show();
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    
        
        private void updateTableRowCss(TableRow<MyObject> rowObj, MyObject item) {
            // On doit vérifier si null
            if (item != null ) {
                if (item.isKeyboardSelected()) {
                    rowObj.setStyle("-fx-background-color: #FF000080;");
                } else {
                    rowObj.setStyle("");
                }
            }
        }
        
        
    }

Final result

Answered By – 4E71-NOP

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published