I need to perform a multiway selection among multiple JavaFX TableView
s such that when selecting one or more rows in one TableView
, all other TableView
s 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 TableView
s.
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 TableView
s 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.