1

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);
    }

}
GreenZebra
  • 362
  • 6
  • 14
  • 2
    good to provide a mcve, +1 :) Basically, subclass TextFieldTableCell and let it update its editability in updateItem (not near my ide, so can't test right now) - make sure to configure the table's items with an extractor on the selected property such that the list fires an update on change. Alternatively, override its startEdit to do nothing if selected isn't checked or updateIndex .. beware: there are some quirks in cell implementations .. – kleopatra Oct 06 '18 at 10:09
  • 1
    also see https://stackoverflow.com/q/52528697/203657 - the accepted answer has an example which overrides the startEdit of a TextFieldTableCell to do nothing if a condition is met, just replace with your own :) – kleopatra Oct 06 '18 at 10:55
  • Thanks for the hints, kleopatra. I'll have go in the morning after I've read up on some of the things you mentioned (I'm still learning Java ... slowly but surely :-) ). Thanks for the link, too. Much appreciated. – GreenZebra Oct 06 '18 at 11:00
  • @kleopatra I had a go and got it working with a subclassed `TextFieldTableCell`. Thank you. That's so much easier than trying to get the `TextFieldTableCell` object. One question: The only way I could get the extractor to fire a change in the data model was to use `setCheckbox()` in the `setSelectedStateCallback()` listener. Is that the correct way to do it? I've updated my post and added a new MVCE that has the revised (and now working) code after reading your link and this one: https://stackoverflow.com/questions/35279377/javafx-weird-keyeventbehavior?rq=1. – GreenZebra Oct 06 '18 at 23:01
  • 1
    a simple `colCheckbox.setCellFactory(CheckBoxTableCell.forTableColumn(colCheckbox));` seems to work just as expected no additional listener needed. In case you are not aware of it: you can answer and accept your own question - then the solution will be more helpful to others :) – kleopatra Oct 07 '18 at 13:30
  • Oh, I didn't think of reverting to a standard `setCellFactory()`. Silly me! I wasn't sure if it was the done thing to post and accept my own answer but have done so. I also reworded the question, now that I know what I should have asked. Thanks again for your help. It's very much appreciated. – GreenZebra Oct 07 '18 at 20:04

1 Answers1

2

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);
    }

}
GreenZebra
  • 362
  • 6
  • 14