Search code examples
javafxcheckboxtablecolumn

javafx: Toggle multiple CheckBoxTableCell-Checkboxes at once


My Goal

I have an editable table with a boolean column where one can check or uncheck the containing checkboxes and a multiple row selection policy. I would like my table to automatically toggle all checkboxes of all selected rows as soon as i check or uncheck one. Don't know if you get the point :D I mean:

  1. I select multiple rows
  2. I check or uncheck the checkbox of one of those selected rows
  3. All other rows's checkboxes get automatically checked or unchecked

Now it should be clear ;)

My Question

(I'm new to JavaFX! I've already done the same thing I'm asking for with AWT/SWING, but am not able to get it working with JavaFX)

Is there similar already built in JavaFX? If not, what is the best aproch to get to my goal?

What I've done so far

I've found out, that you can listen for a change event by setting a CheckBoxTableCell-Callback to the desired Column's CellFactory. I did it like so:

TableColumn<FileSelection, Boolean> selectedColumn = new TableColumn<>("Sel");
selectedColumn.setCellValueFactory(new PropertyValueFactory<>("selected"));
selectedColumn.setCellFactory(CheckBoxTableCell.forTableColumn(rowidx -> {
    if (tblVideoFiles.getSelectionModel().isSelected(rowidx)) {
        tblVideoFiles.getSelectionModel().getSelectedItems().forEach(item -> {
            if (!item.getFile().equals(tblVideoFiles.getItems().get(rowidx).getFile())) {
                item.selectedProperty().set(!item.selectedProperty().get());
             }
         });
     }
     return fileList.get(rowidx).selectedProperty();
}));

The problem here: As soon as a checkbox gets changed, it toggels itself, resulting in a toggle-loop of checking and unchecking itself :D How can I stop this?


Solution

  • I think this is best done directly through the model/selection model, rather than through the cell factory. Here's one approach. The basic idea is:

    1. Create an observable list that has an extractor mapping to the property of the table items representing whether or not that item is selected (in the sense of the check box in the table)
    2. Use a listener on the table's selected items to ensure the list created in step 1 always contains the items that are selected in the table
    3. Add a listener to the list created in step 1 that listens for updates; i.e. changes in the properties representing whether the items are checked via the check box.
    4. If a table-selected item's checked property changes, update all the selected item's checked properties to match. Set a flag ensuring that those changes are ignored by the listener.

    Here's a quick example. First a simple table model class:

    import javafx.beans.property.BooleanProperty;
    import javafx.beans.property.SimpleBooleanProperty;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    
    public class Item {
        private final StringProperty name = new SimpleStringProperty();
        private final BooleanProperty selected = new SimpleBooleanProperty();
        
        public Item(String name) {
            setName(name);
            setSelected(false);
        }
        
        public final StringProperty nameProperty() {
            return this.name;
        }
        
        public final String getName() {
            return this.nameProperty().get();
        }
        
        public final void setName(final String name) {
            this.nameProperty().set(name);
        }
        
        public final BooleanProperty selectedProperty() {
            return this.selected;
        }
        
        public final boolean isSelected() {
            return this.selectedProperty().get();
        }
        
        public final void setSelected(final boolean selected) {
            this.selectedProperty().set(selected);
        }
        
    }
    

    and then a sample app:

    import javafx.application.Application;
    import javafx.beans.Observable;
    import javafx.collections.FXCollections;
    import javafx.collections.ListChangeListener;
    import javafx.collections.ListChangeListener.Change;
    import javafx.collections.ObservableList;
    import javafx.scene.Scene;
    import javafx.scene.control.SelectionMode;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.control.cell.CheckBoxTableCell;
    import javafx.scene.layout.BorderPane;
    import javafx.stage.Stage;
    
    public class App extends Application {
    
        @Override
        public void start(Stage stage) {
            TableView<Item> table = new TableView<>();
            table.setEditable(true);
    
            TableColumn<Item, String> itemCol = new TableColumn<>("Item");
            itemCol.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
            table.getColumns().add(itemCol);
    
            TableColumn<Item, Boolean> selectedCol = new TableColumn<>("Select");
            selectedCol.setCellValueFactory(cellData -> cellData.getValue().selectedProperty());
    
            selectedCol.setCellFactory(CheckBoxTableCell.forTableColumn(selectedCol));
    
            table.getColumns().add(selectedCol);
    
            table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
    
            // observable list of items that fires updates when the selectedProperty of
            // any item in the list changes:
            ObservableList<Item> selectionList = FXCollections
                    .observableArrayList(item -> new Observable[] { item.selectedProperty() });
    
            // bind contents to items selected in the table:
            table.getSelectionModel().getSelectedItems().addListener(
                    (Change<? extends Item> c) -> selectionList.setAll(table.getSelectionModel().getSelectedItems()));
    
            // add listener so that any updates in the selection list are propagated to all
            // elements:
            selectionList.addListener(new ListChangeListener<Item>() {
    
                private boolean processingChange = false;
    
                @Override
                public void onChanged(Change<? extends Item> c) {
                    if (!processingChange) {
                        while (c.next()) {
                            if (c.wasUpdated() && c.getTo() - c.getFrom() == 1) {
                                boolean selectedVal = c.getList().get(c.getFrom()).isSelected();
                                processingChange = true;
                                table.getSelectionModel().getSelectedItems()
                                        .forEach(item -> item.setSelected(selectedVal));
                                processingChange = false;
                            }
                        }
                    }
                }
    
            });
    
            for (int i = 1; i <= 20; i++) {
                table.getItems().add(new Item("Item " + i));
            }
    
            Scene scene = new Scene(new BorderPane(table));
            stage.setScene(scene);
            stage.show();
        }
    
        public static void main(String[] args) {
            launch();
        }
    
    }
    

    Note that there's an (annoying) rule that observable lists should not be changed while a change is being processed (e.g. by a listener). I'm not sure if this completely obeys that rule, as it changes the properties of an observable list which are part of the extractor, while a listener is processing those changes. I think the rule only applies to adding/removing elements, not to updating them, and this code seems to work. However, you might want a hack-around that wraps the code that updates the selected items in a Platform.runLater(...) to ensure compliance with this rule:

            @Override
            public void onChanged(Change<? extends Item> c) {
                if (!processingChange) {
                    while (c.next()) {
                        if (c.wasUpdated() && c.getTo() - c.getFrom() == 1) {
                            boolean selectedVal = c.getList().get(c.getFrom()).isSelected();
                            Platform.runLater(() -> {
                                processingChange = true;
                                table.getSelectionModel().getSelectedItems()
                                    .forEach(item -> item.setSelected(selectedVal));
                                processingChange = false;
                            });
                        }
                    }
                }
            }