javafxtableviewjavafx-8changelistener

How to set the editability of a TextFieldTableCell based on whether or not a CheckBoxTableCell is selected in a JavaFX8 TableView?


I'm trying to set the editability of a TextFieldTableCell depending on whether or not a CheckBoxTableCell (that's in the same row) is ticked. So, for example, if the check box in the second row is ticked as shown below, the text at "B" should be editable. If the check box is unticked, "B" should not be editable.

Example

My plan is to set the TextFieldTableCell's editability in the "selected" listener in the CheckBoxTableCell's setCellFactory. Alternatively, I could set it in the TableView's ListChangeListener.

However, either way, I first have to get the TextFieldTableCell object that's in the same row as the clicked CheckBoxTableCell.

How do I do that? I've been stuck for a couple of days trying to figure it out.

Here's a code snippet for the CheckBoxTableCell's "selected" listener that shows what I'm trying to do and where I'm stuck:

selected.addListener((ObservableValue<? extends Boolean> obs, Boolean wasSelected, Boolean isSelected) -> {
    olTestModel.get(cbCell.getIndex()).setCheckbox(isSelected);
//=>TextFieldTableCell theTextFieldInThisRow = <HOW_DO_I_GET_THIS?>
    theTextFieldInThisRow.setEditable(isSelected);
});

I've read and experimented with Make individual cell editable in JavaFX tableview, Javafx, get the object referenced by a TableCell and Tableview make specific cell or row editable. While I think I understand them, I haven't been able to adapt them to do what I'm trying to do.

Here is the MVCE for the example shown above.

I'm using JavaFX8 (JDK1.8.0_181), NetBeans 8.2 and Scene Builder 8.3.

package test24;

import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Callback;

public class Test24 extends Application {

    private Parent createContent() {

        //********************************************************************************************
        //Declare the TableView and its underlying ObservableList and change listener
        TableView<TestModel> table = new TableView<>();

        ObservableList<TestModel> olTestModel = FXCollections.observableArrayList(testmodel -> new Observable[] {
                testmodel.checkboxProperty()
        });

        olTestModel.addListener((ListChangeListener.Change<? extends TestModel > c) -> {
            while (c.next()) {
                if (c.wasUpdated()) {
                    boolean checkBoxIsSelected = olTestModel.get(c.getFrom()).getCheckbox().booleanValue();
                    //PLAN A:  Set editability here
                    //==>TextFieldTableCell theTextFieldInThisRow = <HOW_DO_I_GET_THIS?>
                    //theTextFieldInThisRow.setEditable(checkBoxIsSelected);
                } 
            }
        });

        olTestModel.add(new TestModel(false, "A"));
        olTestModel.add(new TestModel(false, "B"));
        olTestModel.add(new TestModel(false, "C"));

        table.setItems(olTestModel);

        //********************************************************************************************
        //Declare the text column whose editability needs to change depending on whether or
        //not the CheckBox is ticked
        TableColumn<TestModel, String> colText = new TableColumn<>("text");
        colText.setCellValueFactory(cellData -> cellData.getValue().textProperty());        
        colText.setCellFactory(TextFieldTableCell.<TestModel>forTableColumn());        
        colText.setEditable(false);

        //********************************************************************************************
        //Declare the CheckBox column
        TableColumn<TestModel, Boolean> colCheckbox = new TableColumn<>("checkbox");

        colCheckbox.setCellValueFactory(cellData -> cellData.getValue().checkboxProperty());

        colCheckbox.setCellFactory((TableColumn<TestModel, Boolean> cb) -> {

            final CheckBoxTableCell cbCell = new CheckBoxTableCell<>();
            final BooleanProperty selected = new SimpleBooleanProperty();

            cbCell.setSelectedStateCallback(new Callback<Integer, ObservableValue<Boolean>>() {
                @Override
                public ObservableValue<Boolean> call(Integer index) {
                    return selected;
                }
            });

            selected.addListener((ObservableValue<? extends Boolean> obs, Boolean wasSelected, Boolean isSelected) -> {
                //Set the value in the data model
                olTestModel.get(cbCell.getIndex()).setCheckbox(isSelected);
                //PLAN B:  Set editability here
                //Set the editability for the text field in this row
                //==>   TextFieldTableCell theTextFieldInThisRow = <HOW_DO_I_GET_THIS?>
                //theTextFieldInThisRow.setEditable(isSelected);
            });

            return cbCell;
        });        

        //********************************************************************************************
        //Column to show what's actually in the TableView's data model for the checkbox
        TableColumn<TestModel, Boolean> colDMVal = new TableColumn<>("data model value");
        colDMVal.setCellValueFactory(cb -> cb.getValue().checkboxProperty());
        colDMVal.setEditable(false);

        table.getSelectionModel().setCellSelectionEnabled(true);
        table.setEditable(true);

        table.getColumns().add(colCheckbox);
        table.getColumns().add(colDMVal);
        table.getColumns().add(colText);

        BorderPane content = new BorderPane(table);

        return content;

    }

    public class TestModel {

        private BooleanProperty checkbox;
        private StringProperty text;

        public TestModel() {
            this(false, "");
        }

        public TestModel(
            boolean checkbox,
            String text
        ) {
            this.checkbox = new SimpleBooleanProperty(checkbox);
            this.text = new SimpleStringProperty(text);
        }

        public Boolean getCheckbox() {
            return checkbox.get();
        }

        public void setCheckbox(boolean checkbox) {
            this.checkbox.set(checkbox);
        }

        public BooleanProperty checkboxProperty() {
            return checkbox;
        }

        public String getText() {
            return text.get();
        }

        public void setText(String text) {
            this.text.set(text);
        }

        public StringProperty textProperty() {
            return text;
        }

    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.setTitle("Test");
        stage.setWidth(500);
        stage.show();
    }

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

}

Solution

  • After applying user kleopatra's suggestions, I was able to get this working.

    The easiest solution is to simply ignore edits if the CheckBoxTableCell isn't ticked. This is described in the accepted answer here TreeTableView : setting a row not editable and is what I've used in the MVCE below.

    Alternatively, the TextFieldTableCell's editability can be set by binding its editableProperty() to the CheckBoxTableCell's value. Following the accepted answer here JavaFX weird (Key)EventBehavior, the code looks like this:

    @Override
    public void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);
        doUpdate(item, getIndex(), empty);
    }
    
    @Override
    public void updateIndex(int index) {
        super.updateIndex(index);
        doUpdate(getItem(), index, isEmpty());
    }
    
    private void doUpdate(String item, int index, boolean empty) {
        if ( empty || index == getTableView().getItems().size() ) {
            setText(null);
        } else {
            BooleanProperty checkboxProperty = getTableView().getItems().get(getIndex()).checkboxProperty();
            editableProperty().bind(checkboxProperty);
        }
    }
    

    While the solution is not based on getting the TextFieldTableCell's object (which was what I thought I needed to do), it does do exactly what I need (to set a text field's editability based on a checkbox value). Thank you kleopatra for pointing me in the right direction.

    Here is the MVCE that demonstrates the solution.

    package test24;
    
    import javafx.application.Application;
    import static javafx.application.Application.launch;
    import javafx.beans.Observable;
    import javafx.beans.property.BooleanProperty;
    import javafx.beans.property.SimpleBooleanProperty;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    import javafx.collections.FXCollections;
    import javafx.collections.ListChangeListener;
    import javafx.collections.ObservableList;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.scene.control.TableCell;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.control.cell.CheckBoxTableCell;
    import javafx.scene.control.cell.TextFieldTableCell;
    import javafx.scene.layout.BorderPane;
    import javafx.stage.Stage;
    import javafx.util.converter.DefaultStringConverter;
    
    public class Test24 extends Application {
    
        private Parent createContent() {
    
            //********************************************************************************************
            //Declare the TableView and its underlying ObservableList and change listener
            TableView<TestModel> table = new TableView<>();
    
            ObservableList<TestModel> olTestModel = FXCollections.observableArrayList(testmodel -> new Observable[] {
                    testmodel.checkboxProperty()
            });
    
            olTestModel.addListener((ListChangeListener.Change<? extends TestModel > c) -> {
                while (c.next()) {
                    if (c.wasUpdated()) {
                        //...
                    } 
                }
            });
    
            olTestModel.add(new TestModel(false, "A"));
            olTestModel.add(new TestModel(false, "B"));
            olTestModel.add(new TestModel(false, "C"));
    
            table.setItems(olTestModel);
    
            //********************************************************************************************
            //Declare the CheckBox column
            TableColumn<TestModel, Boolean> colCheckbox = new TableColumn<>("checkbox");
            colCheckbox.setCellValueFactory(cellData -> cellData.getValue().checkboxProperty());
            colCheckbox.setCellFactory(CheckBoxTableCell.forTableColumn(colCheckbox));
    
            //********************************************************************************************
            //Declare the text column whose editability needs to change depending on whether or
            //not the CheckBox is ticked
            TableColumn<TestModel, String> colText = new TableColumn<>("text");
            colText.setCellValueFactory(cellData -> cellData.getValue().textProperty());
            //Don't setEditable() to false here, otherwise updateItem(), updateIndex() and startEdit() won't fire
            colText.setEditable(true);
            colText.setCellFactory(cb -> {
    
                DefaultStringConverter converter = new DefaultStringConverter();
                TableCell<TestModel, String> cell = new TextFieldTableCell<TestModel, String>(converter) {
    
                    @Override
                    public void startEdit() {
                        boolean checkbox = getTableView().getItems().get(getIndex()).getCheckbox();
                        if ( checkbox == true ) {
                            super.startEdit();
                        }
                    }
    
                };
    
                return cell;
    
            });
    
            //********************************************************************************************
            //Column to show what's actually in the TableView's data model for the checkbox
            TableColumn<TestModel, Boolean> colDMVal = new TableColumn<>("data model value");
            colDMVal.setCellValueFactory(cb -> cb.getValue().checkboxProperty());
            colDMVal.setEditable(false);
    
            table.getSelectionModel().setCellSelectionEnabled(true);
            table.setEditable(true);
    
            table.getColumns().add(colCheckbox);
            table.getColumns().add(colDMVal);
            table.getColumns().add(colText);
    
            BorderPane content = new BorderPane(table);
    
            return content;
    
        }
    
        public class TestModel {
    
            private BooleanProperty checkbox;
            private StringProperty text;
    
            public TestModel() {
                this(false, "");
            }
    
            public TestModel(
                boolean checkbox,
                String text
            ) {
                this.checkbox = new SimpleBooleanProperty(checkbox);
                this.text = new SimpleStringProperty(text);
            }
    
            public Boolean getCheckbox() {
                return checkbox.get();
            }
    
            public void setCheckbox(boolean checkbox) {
                this.checkbox.set(checkbox);
            }
    
            public BooleanProperty checkboxProperty() {
                return checkbox;
            }
    
            public String getText() {
                return text.get();
            }
    
            public void setText(String text) {
                this.text.set(text);
            }
    
            public StringProperty textProperty() {
                return text;
            }
    
        }
    
        @Override
        public void start(Stage stage) throws Exception {
            stage.setScene(new Scene(createContent()));
            stage.setTitle("Test");
            stage.setWidth(500);
            stage.show();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    
    }