I am looking to create a custom TableColumn which is a CheckBox control which represents if the row is selected or not. We don't want to use the standard SHIFT or CONTROL + CLICK to handle this as users tend to click inadvertantly and lose their selection.
What I would like to do is the following:
In this case the column should not be part of the underlying data model, and simply represent what is selected or not. It shouldn't be dependent on the data model in any way. If the user selects the check box in the column header, it should select all the records in the table (not just the visible ones) and if it's deselected, it should then deselect all the records.
Is there a way to represent the selection model in this way?
The second issue I had, was my first attempt will not show up as selectable in SceneBuilder since (from what I can gather) the TableColumn isn't a Node and therefore seems to be ignored from import?
Any help would be greatly appreciated.
Thanks
A possible solution could be extending TableViewSkin
to add a TableColumn
with the checkboxes, while this column is not baked with the user's model, but changes on the checkboxes selection will affect the table's selection model.
While selecting only through the checkboxes works fine, you can't remove the behavior that allows selecting the rows as well, so you have to listen for both cases.
This snippet works, but it hasn't been tested with sorting and modifying the model, so it will be just a start for a custom TableView
.
CheckTableView class
public class CheckTableView<T> extends TableView<T> {
private ObservableList<T> selected;
public CheckTableView() {
this(FXCollections.observableArrayList());
}
public CheckTableView(ObservableList<T> items) {
setItems(items);
setEditable(true);
getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
skinProperty().addListener(new InvalidationListener() {
@Override
public void invalidated(Observable observable) {
selected = ((CheckTableViewSkin) getSkin()).getSelectedRows();
skinProperty().removeListener(this);
}
});
}
@Override
protected Skin<?> createDefaultSkin() {
return new CheckTableViewSkin<>(this);
}
public ObservableList<T> getSelectedRows() {
return selected;
}
}
CheckTableViewSkin class
public class CheckTableViewSkin<T> extends TableViewSkin<T> {
private final TableColumn<T, Boolean> checkColumn;
private final CheckBox headerCheckBox = new CheckBox();
private final List<BooleanProperty> colSelected = new ArrayList<>();
private final ChangeListener<Number> listener = (obs, ov, nv) -> {
if (nv.intValue() != -1) {
Platform.runLater(() -> {
colSelected.get(nv.intValue()).set(!colSelected.get(nv.intValue()).get());
refreshSelection();
});
}
};
private final ChangeListener<Boolean> headerListener = (obs, ov, nv) -> {
Platform.runLater(() -> {
IntStream.range(0, colSelected.size()).forEach(i -> colSelected.get(i).set(nv));
refreshSelection();
});
};
public CheckTableViewSkin(CheckTableView<T> control) {
super(control);
checkColumn = new TableColumn<>();
headerCheckBox.selectedProperty().addListener(headerListener);
checkColumn.setGraphic(headerCheckBox);
// install listeners in checkboxes
IntStream.range(0, control.getItems().size()).forEach(i -> {
final SimpleBooleanProperty simple = new SimpleBooleanProperty();
simple.addListener((obs, ov, nv) -> refreshSelection());
colSelected.add(simple);
});
checkColumn.setCellFactory(CheckBoxTableCell.forTableColumn(colSelected::get));
checkColumn.setPrefWidth(60);
checkColumn.setEditable(true);
checkColumn.setResizable(false);
getColumns().add(0, checkColumn);
getSelectionModel().selectedIndexProperty().addListener(listener);
}
private void refreshSelection() {
// refresh list of selected rows
getSelectionModel().selectedIndexProperty().removeListener(listener);
getSelectionModel().clearSelection();
AtomicInteger count = new AtomicInteger();
IntStream.range(0, colSelected.size()).forEach(i -> {
if (colSelected.get(i).get()) {
getSelectionModel().select(i);
count.getAndIncrement();
}
});
headerCheckBox.selectedProperty().removeListener(headerListener);
headerCheckBox.setSelected(count.get() == colSelected.size());
headerCheckBox.selectedProperty().addListener(headerListener);
// it may flick, but required to show all selected rows focused
getSkinnable().requestFocus();
getSelectionModel().selectedIndexProperty().addListener(listener);
}
public ObservableList<T> getSelectedRows() {
return getSelectionModel().getSelectedItems();
}
}
Now the sample, with the Application
class:
@Override
public void start(Stage primaryStage) {
TableColumn<Person, String> firstNameColumn = new TableColumn<>("First Name");
firstNameColumn.setCellValueFactory(p -> p.getValue().firstNameProperty());
TableColumn<Person, String> lastNameColumn = new TableColumn<>("Last Name");
lastNameColumn.setCellValueFactory(p -> p.getValue().lastNameProperty());
CheckTableView<Person> tableView = new CheckTableView(FXCollections.observableArrayList(
new Person("Hans", "Muster"), new Person("Ruth", "Mueller"),
new Person("Heinz", "Kurz"), new Person("Cornelia", "Meier"),
new Person("Anna", "Best"), new Person("Stefan", "Meier")
));
tableView.getColumns().addAll(firstNameColumn, lastNameColumn);
Scene scene = new Scene(tableView, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
for the usual Person
class:
public class Person {
private final StringProperty firstName;
private final StringProperty lastName;
public Person(String firstName, String lastName) {
this.firstName = new SimpleStringProperty(firstName);
this.lastName = new SimpleStringProperty(lastName);
}
//getters & setters
}
This custom control will work with Scene Builder as well.