2

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();
        }
}
Kay
  • 23
  • 6

1 Answers1

2

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

}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • I see so its better to add the editableProperty within the Item model right? , thanks a lot I will try this and see how it goes. I have edited my question with a full example. (It should be Item yes) – Kay May 19 '20 at 12:34
  • @Kay Not sure what is "better": it really depends on whether that information is naturally part of the data in the model (put it in the model), or just an aspect of the UI (do as done here). The code will basically look very similar either way. – James_D May 19 '20 at 12:36
  • thanks!! your example works like a charm its very helpful. I have another issue regarding this . I use a custom Textfield for the cell that uses JFX design as I don't want to use the normal one. how should I update the quantity to be able to use the disable feature and a custom cell factory? – Kay May 19 '20 at 14:25
  • @Kay You should be able to adapt your existing custom cell factory to include the functionality that binds the editable property to the observable set (or however you want to manage that logic). Just add the same logic into your existing `updateItem` method in the cell. – James_D May 19 '20 at 14:28
  • sorry about that another issue came up. It re enables editing if I leave the scene. I want it permanently disabled so only once edited and thats it the user is not allowed to come back and edit after leaving the scene – Kay May 20 '20 at 14:13
  • Sounds like you just need to scope the set of previously-edited items more broadly, so you're not resetting it when you reload the scene. – James_D May 20 '20 at 14:54
  • hmm..I don't understand why its resetting even when I click a button within the same scene it resets. I mean if it has been disabled already shouldn't it stay disabled? where about in the code is it getting re enabled again? in the same scene I mean. I think I might add a boolean disabled in the model and update that based on the row ID whenever the scene reloads or any other clicks. Would that be an efficient way? – Kay May 20 '20 at 15:33
  • @Kay It should stay disabled as long as the item in the table is in the observable set. If you, e.g., reload the table data with different instances of the model class, or somehow reset the observable set, then editing will become enabled again. You can fix the first with an appropriate `equals()` method in the model class. But hard to figure out why it's not working without the ability to debug... – James_D May 20 '20 at 15:49
  • Would you be able to show an example of the equal() I'm a bit confused I have tried to add a boolean and set the boolean in setOnEditCommit to true then check in the updateItem to bind but it in this case it disables the whole quantity column. I feel like I'm not understaning something here which I cannot figure out – Kay May 20 '20 at 18:00
  • I've managed to fix the problem using startEdit thank you very much for your help and guidance! really appreciate it! – Kay May 21 '20 at 11:52