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

Time:02-01

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.

CodePudding user response:

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.

  • Related