Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a more idiomatic way to perform mutual, multi-way selection using TableViews?

Tags:

java

javafx

I need to perform a multiway selection among multiple JavaFX TableViews such that when selecting one or more rows in one TableView, all other TableViews with related information will also be selected/highlighted (actually, highlighting is more appropriate, but selection is built-in, so...)

I've come up with a slighly janky method, but it doesn't really scale very well to many TableViews.

package multiwayselect;

import javafx.application.*;
import javafx.beans.property.*;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.control.TableView.TableViewSelectionModel;
import javafx.scene.layout.*;
import javafx.stage.*;

public class MultiwaySelectDemo extends Application {

    private final ObservableList<Part> parts = FXCollections.observableArrayList();
    private final ObservableList<Assembly> assemblies = FXCollections.observableArrayList();

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

    @Override
    public void start(Stage stage) throws Exception {
        buildModel();
        stage.setTitle("multi-way selection demo");
        final Region root = buildView();
        stage.setScene(new Scene(root));
        stage.show();
    }

    private void buildModel() {
        Part cpu = new Part(1, "CPU Ryzen 5");
        Part ram8 = new Part(2, "RAM 8GB DDR4 (1x8GB)");
        Part ram16 = new Part(5, "RAM 16GB DDR4 (2x8GB)");
        Part mobo1 = new Part(3, "MOBO ATX B550");
        Part mobo2 = new Part(7, "MOBO ATX X570 RGB");
        Part chassis = new Part(4, "CASE Standard ATX Case");
        Part chassis1 = new Part(8, "CASE Gamer ATX Case w/RGB");
        Assembly basicBox = new Assembly(1, "Basic AMD Box", cpu, ram8, mobo1, chassis);
        Assembly gamerBox = new Assembly(2, "Gamer AMD Box", cpu, ram16, mobo2, chassis1);
        assemblies.addAll(basicBox, gamerBox);
        for (Assembly a : assemblies) {
            for (Part p : a.parts) {
                if (!parts.contains(p)) {
                    parts.add(p);
                }
            }
        }
    }

    private boolean selecting = false;

    private Region buildView() {
        TableView<Part> partsTable = setupPartsTableView();
        TableView<Assembly> assembliesTable = setupAssembliesTableView();
        assembliesTable.getSelectionModel().getSelectedItems().addListener(new ListChangeListener<Assembly>() {
            @Override
            public void onChanged(Change<? extends Assembly> c) {
                if (!selecting) {
                    selecting = true;

                    TableViewSelectionModel<Part> sm = partsTable.getSelectionModel();
                    sm.clearSelection();
                    for (Assembly a : c.getList()) {
                        for (Part p : a.partsProperty()) {
                            sm.select(p);
                        }
                    }
                    selecting = false;
                }

            }
        });

        partsTable.getSelectionModel().getSelectedItems().addListener(new ListChangeListener<Part>() {
            @Override
            public void onChanged(Change<? extends Part> c) {
                if (!selecting) {
                    selecting = true;
                    TableViewSelectionModel<Assembly> sm = assembliesTable.getSelectionModel();
                    sm.clearSelection();
                    for (Part p : c.getList()) {
                        for (Assembly a : assemblies) {
                            if (a.partsProperty().contains(p)) {
                                sm.select(a);
                            }
                        }
                    }
                    selecting = false;
                }
            }
        });
        return new SplitPane(assembliesTable, partsTable);
    }

    private TableView setupAssembliesTableView() {
        final TableView tableView = new TableView(assemblies);
        tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

        final TableColumn<Assembly, Integer> idColumn = new TableColumn<>("id");
        idColumn.setCellValueFactory(cell -> cell.getValue().idProperty());

        final TableColumn<Assembly, String> nameColumn = new TableColumn<>("name");
        nameColumn.setCellValueFactory(cell -> cell.getValue().nameProperty());

        tableView.getColumns().addAll(idColumn, nameColumn);
        return tableView;
    }

    private TableView setupPartsTableView() {
        final TableView tableView = new TableView(parts);
        tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

        final TableColumn<Part, Integer> idColumn = new TableColumn<>("id");
        idColumn.setCellValueFactory(cell -> cell.getValue().idProperty());

        final TableColumn<Part, String> nameColumn = new TableColumn<>("name");
        nameColumn.setCellValueFactory(cell -> cell.getValue().nameProperty());

        tableView.getColumns().addAll(idColumn, nameColumn);

        return tableView;
    }

    public static class Part {

        private ObjectProperty<Integer> id;
        private StringProperty name;

        public Part(int newId, String newName) {
            this.id = new SimpleObjectProperty<>();
            this.name = new SimpleStringProperty();
            setId(newId);
            setName(newName);
        }

        public final ObjectProperty<Integer> idProperty() {
            return this.id;
        }

        public final void setId(int newValue) {
            idProperty().set(newValue);
        }

        public final int getId() {
            return idProperty().get();
        }

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

        public final void setName(String newValue) {
            nameProperty().set(newValue);
        }

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

    public static class Assembly {

        private ObjectProperty<Integer> id;
        private StringProperty name;
        private ObservableList<Part> parts;

        public Assembly(int newId, String newName, Part... newParts) {
            id = new SimpleObjectProperty<>();
            name = new SimpleStringProperty();
            parts = FXCollections.observableArrayList();
            setId(newId);
            setName(newName);
            parts.setAll(newParts);
        }

        public final ObjectProperty<Integer> idProperty() {
            return id;
        }

        public final void setId(int newId) {
            idProperty().set(newId);
        }

        public final int getId() {
            return idProperty().get();
        }

        public final StringProperty nameProperty() {
            return name;
        }

        public final void setName(String newValue) {
            nameProperty().set(newValue);
        }

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

        public final ObservableList<Part> partsProperty() {
            return parts;
        }
    }

}

Basically selecting puts up an "in selection mode" mutex; without it, the two listeners eat up the stack.

What's a better way to do this that (a) scales out to many TableViews and/or (b) is more idiomatic of JavaFX or (c) is just plain better? (Note: currently working with Java 8/JavaFX 8 but will take solutions in higher versions.) Also, will take any feedback on different ways to represent the data that better fits the JavaFX idiom.

like image 482
Brad K. Avatar asked Oct 22 '25 04:10

Brad K.


1 Answers

I don't think there is a fundamentally simpler way of doing what you're already doing. You can refactor this though in a way that might be more "scalable":

public class SelectionManager {
    private boolean selecting ;
    private <S,T> void setUpMultiSelection(TableView<S> firstTable, TableView<T> secondTable, BiPredicate<S,T> shouldSelect) {
        firstTable.getSelectionModel().getSelectedItems().addListener((Change<? extends S> c) -> {
            if (selecting) return;
            selecting = true ;
            secondTable.getSelectionModel().clearSelection();
            for (T t : secondTable.getItems()) {
                for (S s : firstTable.getSelectionModel().getSelectedItems()) {
                    if (shouldSelect.test(s, t)) {
                        secondTable.getSelectionModel().select(t);
                    }
                }
            }
            selecting = false;
        });
        secondTable.getSelectionModel().getSelectedItems().addListener((Change<? extends T> c) -> {
            if (selecting) return ;
            selecting = true;
            firstTable.getSelectionModel().clearSelection();
            for (S s : firstTable.getItems()) {
                for (T t : secondTable.getSelectionModel().getSelectedItems()) {
                    if (shouldSelect.test(s, t)) {
                        firstTable.getSelectionModel().select(s);
                    }
                }
            }
            selecting = false;
        });
    }
}

and then

    private Region buildView() {
        TableView<Part> partsTable = setupPartsTableView();
        TableView<Assembly> assembliesTable = setupAssembliesTableView();
//        assembliesTable.getSelectionModel().getSelectedItems().addListener(new ListChangeListener<Assembly>() {
//            @Override
//            public void onChanged(Change<? extends Assembly> c) {
//                if (!selecting) {
//                    selecting = true;
//
//                    TableViewSelectionModel<Part> sm = partsTable.getSelectionModel();
//                    sm.clearSelection();
//                    for (Assembly a : c.getList()) {
//                        for (Part p : a.partsProperty()) {
//                            sm.select(p);
//                        }
//                    }
//                    selecting = false;
//                }
//
//            }
//        });
//
//        partsTable.getSelectionModel().getSelectedItems().addListener(new ListChangeListener<Part>() {
//            @Override
//            public void onChanged(Change<? extends Part> c) {
//                if (!selecting) {
//                    selecting = true;
//                    TableViewSelectionModel<Assembly> sm = assembliesTable.getSelectionModel();
//                    sm.clearSelection();
//                    for (Part p : c.getList()) {
//                        for (Assembly a : assemblies) {
//                            if (a.partsProperty().contains(p)) {
//                                sm.select(a);
//                            }
//                        }
//                    }
//                    selecting = false;
//                }
//            }
//        });
        SelectionManager selectionManager = new SelectionManager();
        selectionManager.setUpMultiSelection(assembliesTable, partsTable,
                (assembly, part) -> assembly.partsProperty().contains(part)
        );

        return new SplitPane(assembliesTable, partsTable);
    }

The point to note here is that if you use the same instance of SelectionManager for multiple "multi-selections", it will use the same mutex. On the other hand, a different SelectionManager instance will have its own mutex. This (or some variation on it) should enable you to fairly easily configure this over many tables in any way you need.

like image 64
James_D Avatar answered Oct 27 '25 01:10

James_D