javauitableviewjavafxtablecell

disable TableCell after setOnEditCommit JavaFX


I have created an editable table that has a certain column (quantityColumn) that only this column's cells should be disabled and not editable anymore when setOnEditCommit is done. So in other words they are only allowed to enter once. The program has an add row button that adds new rows to the table and setOnEditCommit updates the database with the new values from each cell in each column.

An example of how my code looks like:

public class TableViewController implements Initializable {

/**
 * Initializes the controller class.
 */
private Connection c = ConnectDB.getInstance().getConnection(); // ConnectDB is a helper that connects to the database
@FXML
private TableView<Item> table;

@FXML
private TableColumn<Item, LocalDate> dateColumn;

@FXML
private TableColumn<Item, LocalTime> timeColumn;

@FXML
private TableColumn<Item, String> quantityColumn;

@Override
public void initialize(URL url, ResourceBundle rb) {


 //the following three are custom TableCell classes that allows me to make my cell have a JFX date and time picker.

    Callback<TableColumn<Item, LocalDate>, TableCell<Item, LocalDate>> cellFactoryDate = 
            (TableColumn<Item, LocalDate> param) -> new DatePickerCell();
    Callback<TableColumn<Item, LocalTime>, TableCell<Item, LocalTime>> cellFactoryTime = 
            (TableColumn<Item, LocalTime> param) -> new TimePickerCell();
    Callback<TableColumn<Item, String>, TableCell<Item, String>> cellFactoryText = 
            (TableColumn<Item, String> param) -> new JFXTextFieldCell(); 


    dateColumn.setCellValueFactory(cellData -> cellData.getValue().weekDateProperty());
    timeColumn.setCellValueFactory(cellData -> cellData.getValue().timeProperty());
    quantityColumn.setCellValueFactory(cellData -> cellData.getValue().quantityProperty());

    dateColumn.setCellFactory(cellFactoryDate);
    timeColumn.setCellFactory(cellFactoryTime);   
    quantityColumn.setCellFactory(cellFactoryText);   

    table.setEditable(true);

    dateColumn.setOnEditCommit(event -> {
        Item user = event.getRowValue();
        user.setWeekDate(event.getNewValue());
        updateDate("WeekDate", event.getNewValue(), user.getID());
    });
    timeColumn.setOnEditCommit(event -> {
        Item user = event.getRowValue();
        user.setTime(event.getNewValue());
        updateTime("Time", event.getNewValue(),  user.getID());
    });
    quantityColumn.setOnEditCommit(event -> {
        Item user = event.getRowValue();
        user.setQuantity(event.getNewValue());
        updateQuantity("Quantity", event.getNewValue(),  user.getID());

        //I want to disable the cell that has been committed here
    });

}

private void updateDate(String column, LocalDate date, int id) {

    try {

        PreparedStatement stmt = c.prepareStatement("UPDATE Items SET "+column+" = ? WHERE ID = ? ");
        stmt.setDate(1, java.sql.Date.valueOf(date));
        stmt.setInt(2, id);
        stmt.executeUpdate();
    } catch (SQLException ex) {
        System.err.println("Error");
        ex.printStackTrace(System.err);
    }
}

private void updateTime(String column, LocalTime time, int id) {

    try {

        PreparedStatement stmt = c.prepareStatement("UPDATE Items SET "+column+" = ? WHERE ID = ? ");
        stmt.setTime(1, java.sql.Time.valueOf(time));
        stmt.setInt(2, id);
        stmt.executeUpdate();
    } catch (SQLException ex) {
        System.err.println("Error");
        ex.printStackTrace(System.err);
    }
}

private void updateQuantity(String column, String quantity, int id) {

    try {

        PreparedStatement stmt = c.prepareStatement("UPDATE Items SET "+column+" = ? WHERE ID = ? ");
        stmt.setString(1, quantity);
        stmt.setInt(2, id);
        stmt.executeUpdate();
    } catch (SQLException ex) {
        System.err.println("Error");
        ex.printStackTrace(System.err);
    }
}
 @FXML
 private void addRow(ActionEvent event) {

    // get current position
    TablePosition pos = table.getFocusModel().getFocusedCell();

    // clear current selection
    table.getSelectionModel().clearSelection();

    // create new record and add it to the model
    Item data = new Item();
    table.getItems().add(data);
    // get last row
    int row = table.getItems().size() - 1;
    table.getSelectionModel().select( row, pos.getTableColumn());

    // scroll to new row
    table.scrollTo( data);

 }

}

Item:

public class Item {

private final IntegerProperty id;
private final ObjectProperty<LocalDate> weekDate;
private final ObjectProperty<LocalTime> time;
private final StringProperty quantity;


public Item() {
    this(0, null, null, null);
}
/**
 * Constructor with some initial data.
 * @param id
 * @param weekDate
 * @param time
 * @param quantity
 * 
 */
public Item(int id, LocalDate weekDate, LocalTime time, String quantity) {            
    this.id = new SimpleIntegerProperty(id);
    this.weekDate = new SimpleObjectProperty(weekDate);
    this.time = new SimpleObjectProperty(time);
    this.quantity = new SimpleStringProperty(quantity);

}


public int getID() {
    return id.get();
}

public void setID(int id) {
    this.id.set(id);
}

public IntegerProperty idProperty() {
    return id;
}

public LocalDate getWeekDate() {
    return weekDate.get();
}

public void setWeekDate(LocalDate weekDate) {
    this.weekDate.set(weekDate);
}

public ObjectProperty<LocalDate> weekDateProperty() {
    return weekDate;
}


public LocalTime getTime() {
    return time.get();
}

public void setTime(LocalTime time) {
    this.time.set(time);
}

public ObjectProperty<LocalTime> timeProperty() {
    return time;
}

public String getQuantity() {
    return quantity.get();
}

public void setQuantity(String quantity) {
    this.quantity.set(quantity);
}

public StringProperty quantityProperty() {
    return quantity;
}
}

TableView FXML

<AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="disablecelltest.TableViewController">
   <children>
      <TableView fx:id="table" editable="true" layoutX="61.0" layoutY="70.0" prefHeight="292.0" prefWidth="479.0">
        <columns>
          <TableColumn fx:id="dateColumn" prefWidth="75.0" text="Date" />
          <TableColumn fx:id="timeColumn" prefWidth="75.0" text="Time" />
            <TableColumn fx:id="quantityColumn" prefWidth="75.0" text="Quantity" />
        </columns>
      </TableView>
      <JFXButton buttonType="RAISED" contentDisplay="TEXT_ONLY" graphicTextGap="10.0" layoutX="10.0" layoutY="10.0" onAction="#addRow" text="ADD RECORD" textAlignment="CENTER">
         <font>
            <Font name="Dosis SemiBold" size="18.0" />
         </font>
      </JFXButton>
   </children>
</AnchorPane>

I have tried looking for answers here. This is one answer I tried and modified it without condition it disabled the whole column not that certain cell as I don't have a condition for the item I just want to disable when its been entered only once. I tried this as well.

EDIT: This is the custom JFXTextField I'm using for quantity:

public class JFXTextFieldCell extends TableCell<Item, String> {

        private JFXTextField textField;

        public JFXTextFieldCell() {
        }

        @Override
        public void startEdit() {
            if (!isEmpty()) {
                super.startEdit();
                createTextField();
                setText(null);
                setGraphic(textField);
                textField.selectAll();
            }
        }

        @Override
        public void cancelEdit() {
            super.cancelEdit();

            setText((String) getItem());
            setGraphic(null);
        }

        @Override
        public void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);

            if (empty) {
                setText(item);
                setGraphic(null);
            } else {
                if (isEditing()) {
                    if (textField != null) {
                        textField.setText(getString());
//                        setGraphic(null);
                    }
                    setText(null);
                    setGraphic(textField);
                } else {
                    setText(getString());
                    setGraphic(null);
                }
            }
        }

        private void createTextField() {
            textField = new JFXTextField(getString());
            textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
            textField.setOnAction((e) -> commitEdit(textField.getText()));
            textField.focusedProperty().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
                if (!newValue) {
                    commitEdit(textField.getText());
                }
            });
        }

        private String getString() {
            return getItem() == null ? "" : getItem();
        }
}

Solution

  • Simply disabling (or making uneditable) the current cell won't work; cells will be reassigned e.g. if the user scrolls around the table, so the wrong cells would end up not editable.

    You will need to add some property to your model (or store some properties elsewhere which can be accessed via the model instances) and implement a custom cell which observes those properties, updating the editable state appropriately.

    Something like the following should work:

    ObservableSet<Item> quantityEditedItems = FXCollections.observableSet();
    
    quantityColumn.setCellFactory(tc -> new TextFieldTableCell<>(new IntegerStringConverter()) {
        @Override
        public void updateItem(Integer quantity, boolean empty) {
            super.updateItem(quantity, empty) ;
            editableProperty().unbind();
            if (empty) {
                setEditable(false);
            } else {
                editableProperty().bind(Bindings.createBooleanBinding(() ->
                    ! quantityEditedItems.contains(getTableView().getItems().get(getIndex())),
                    quantityEditedItems));
            }
        }
    });
    

    and then you can do

    quantityColumn.setOnEditCommit(event -> {
        StudentPresc user = event.getRowValue(); // should this be Item?
        user.setQuantity(event.getNewValue());
        updateQuantity("Quantity", event.getNewValue(),  user.getID());
    
        quantityEditedItems.add(event.getRowValue());
    });
    

    Here's a complete working example:

    import java.util.Random;
    
    import javafx.application.Application;
    import javafx.beans.binding.Bindings;
    import javafx.beans.property.IntegerProperty;
    import javafx.beans.property.SimpleIntegerProperty;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableSet;
    import javafx.collections.SetChangeListener.Change;
    import javafx.scene.Scene;
    import javafx.scene.control.Label;
    import javafx.scene.control.ListCell;
    import javafx.scene.control.ListView;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.control.cell.TextFieldTableCell;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    import javafx.util.converter.IntegerStringConverter;
    
    public class EditOnceTable extends Application {
    
        @Override
        public void start(Stage primaryStage) throws Exception {
    
            TableView<Item> table = new TableView<>();
            table.setEditable(true);
            TableColumn<Item, String> itemColumn = new TableColumn<>("Item");
            itemColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
    
            TableColumn<Item, Integer> quantityColumn = new TableColumn<>("Quantity");
            quantityColumn.setCellValueFactory(cellData -> cellData.getValue().quantityProperty().asObject());
            quantityColumn.setEditable(true);
    
            table.getColumns().add(itemColumn);
            table.getColumns().add(quantityColumn);
    
            ObservableSet<Item> quantityEditedItems = FXCollections.observableSet();
    
            quantityColumn.setCellFactory(tc -> new TextFieldTableCell<>(new IntegerStringConverter()) {
                @Override
                public void updateItem(Integer quantity, boolean empty) {
                    super.updateItem(quantity, empty) ;
                    editableProperty().unbind();
                    if (empty) {
                        setEditable(false);
                    } else {
                        editableProperty().bind(Bindings.createBooleanBinding(() ->
                            ! quantityEditedItems.contains(getTableView().getItems().get(getIndex())),
                            quantityEditedItems));
                    }
                }
            });
    
            quantityColumn.setOnEditCommit(event -> {
                Item item = event.getRowValue(); // should this be Item?
                item.setQuantity(event.getNewValue());
    //          updateQuantity("Quantity", event.getNewValue(),  user.getID());
    
                quantityEditedItems.add(event.getRowValue()) ;
            });
    
            ListView<Item> editedItemsView = new ListView<>();
            quantityEditedItems.addListener((Change<? extends Item> change) -> 
                editedItemsView.getItems().setAll(quantityEditedItems)
            );
            editedItemsView.setCellFactory(lv -> new ListCell<>() {
                @Override
                protected void updateItem(Item item, boolean empty) {
                    super.updateItem(item, empty);
                    if (empty || item==null) {
                        setText("");
                    } else {
                        setText(item.getName());
                    }
                }
            });
            Button clear = new Button("Clear edited");
            clear.setOnAction(e -> quantityEditedItems.clear());
    
            Random rng = new Random();
            for (int i = 1 ; i <= 40 ; i++) {
                table.getItems().add(new Item("Item "+i, rng.nextInt(100)));
            }
    
            BorderPane root = new BorderPane(table);
            root.setRight(new VBox(5, new Label("Edited:"), editedItemsView, clear));
    
            Scene scene = new Scene(root);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
    
        public static class Item {
            private final StringProperty name = new SimpleStringProperty() ;
            private final IntegerProperty quantity = new SimpleIntegerProperty();
    
            public Item(String name, int quantity) {
                setName(name);
                setQuantity(quantity);
            }
    
            public StringProperty nameProperty() {
                return name ;
            }
    
            public final String getName() {
                return nameProperty().get();
            }
    
            public final void setName(String name) {
                nameProperty().set(name);
            }
    
            public IntegerProperty quantityProperty() {
                return quantity ;
            }
    
            public final int getQuantity() {
                return quantityProperty().get();
            }
    
            public final void setQuantity(int quantity) {
                quantityProperty().set(quantity);
            }
        }
    
        public static void main(String[] args) {
            Application.launch(args);
        }
    
    }