Search code examples
javafxcelltextfield

How to add TextField with TextFormatter and Listener into TableCell?


I have Water object with Integer parameter value. This parameter can be empty if not created yet, which equals null. Value will be stored as Integer, but when rendered, converted to Decimal and printed for user as String with TextFormatter applied. After user makes changes it should be converted back into appropriate Integer and stored in database.

My Water object:

private Integer value; 

enter image description here

Code for table:

@FXML
private TableView<Water> waterTable;

@FXML
private TableColumn<Water, Integer> valueColumn;

@FXML
private void initialize(){
    waterTable.setItems(FXCollections.observableArrayList(thisMonthWaterList));

    //don't know how to implemeny here instead of this code cell with TextField
    /*valueColumn.setCellValueFactory(cellData -> new 
    SimpleIntegerProperty(cellData.getValue().getAmount()).asObject());
        valueColumn.setCellFactory(new Callback<TableColumn<Water,Integer>, 
    TableCell<Water,Integer>>() {

            @Override
            public TableCell<Water, Integer> call(TableColumn<Water, Integer> param) {
                return new TableCell<Water, Integer>(){
                    @Override
                    protected void updateItem(Integer value, boolean empty){
                        super.updateItem(value, empty);
                        if(value==null || empty){
                            setText(null);
                        }else{
                            setText(value.toString());
                        }
                    }
                };
            }
        });*/
}

This is the code for TextFormatter:

TextField.setTextFormatter(new TextFormatter<>(change -> {
            int maxLength = 10;

            if (change.isAdded()) {
                if(change.getControlNewText().length()<=maxLength){
                    if (change.getText().contains(",")) {
                        change.setText(change.getText().replaceAll(",", "."));
                    }
                    change = change.getControlNewText().matches("^\\d*(\\.\\d{0,1})?$") ? change : null;
                }else{
                    if(change.getText().length()==1){
                        change = null;
                    }else{
                        int allowedLength = maxLength - change.getControlText().length();
                        change.setText(change.getText().substring(0, allowedLength));
                    }
                }
            }
            return change;
        }));

Code for Listener:

TextField.focusedProperty().addListener(new FocusChangeListener(TextField, text -> {
            if(text.isEmpty()){
                TextField.setText(Water.getAreaFormat());
            }else{
                TextField.setText(Water.toString(TextField.getText()));
            }
            thisObject.setValue(Water.toInt(TextField.getText()));
            WaterDA.update(thisObject);
        }, text -> {
            if(text.equals(Water.getWaterFormat())){
                TextField.setText("");
            }
        }));

Solution

  • Actually you can handle both conversion and preventing invalid input using a TextFormatter using a StringConverter and a UnaryOperator. The following code assumes you've got a ObjectProperty<Integer> in your Water class and the amountProperty() method returns it.

    If the Water.toString(int) or Water.toString(Integer) method does not exist, you need to implement the conversion from int to string for the following code to work.

    private static final StringConverter<Integer> VALUE_CONVERTER = new StringConverter<Integer>() {
    
        @Override
        public String toString(Integer object) {
            return object == null ? Water.getAreaFormat() : Water.toString(object);
        }
    
        @Override
        public Integer fromString(String string) {
            return Water.toInt(string);
        }
    
    };
    
    // filter copied unmodified from your code
    private static final UnaryOperator<TextFormatter.Change> VALUE_FILTER = change -> {
        int maxLength = 10;
    
        if (change.isAdded()) {
            if(change.getControlNewText().length() <= maxLength){
                if (change.getText().contains(",")) {
                    change.setText(change.getText().replaceAll(",", "."));
                }
                change = change.getControlNewText().matches("^\\d*(\\.\\d{0,1})?$") ? change : null;
            } else {
                if (change.getText().length() == 1){
                    change = null;
                } else {
                    int allowedLength = maxLength - change.getControlText().length();
                    change.setText(change.getText().substring(0, allowedLength));
                }
            }
        }
        return change;
    };
    
    @FXML
    private TableColumn<Water, Integer> valueColumn;
    
    @FXML
    private void initialize(){
        waterTable.setItems(FXCollections.observableArrayList(thisMonthWaterList));
    
        valueColumn.setCellValueFactory(cellData -> cellData.getValue().amountProperty());
        valueColumn.setCellFactory(new Callback<TableColumn<Water, Integer>, TableCell<Water, Integer>>() {
    
            @Override
            public TableCell<Water, Integer> call(TableColumn<Water, Integer> param) {
                return new TableCell<Water, Integer>() {
    
                    private final TextFormatter<Integer> formatter;
                    private final TextField textField;
    
                    {
                        textField = new TextField();
                        formatter = new TextFormatter<>(VALUE_CONVERTER, null, VALUE_FILTER);
                        textField.setTextFormatter(formatter);
                        formatter.valueProperty().addListener((o, oldValue, newValue) -> {
                            Water water = (Water) getTableRow().getItem();
                            if (!Objects.equals(water.getAmount(), newValue)) {
                                 // update item and db, if value was modified
                                 water.setAmount(newValue);
                                 WaterDA.update(water);
                            }
                        });
                    }
    
                    @Override
                    protected void updateItem(Integer value, boolean empty){
                        super.updateItem(value, empty);
                        if (empty){
                            setGraphic(null);
                        } else {
                            setGraphic(textField);
                            formatter.setValue(value);
                        }
                    }
                };
            }
        });
    
    }
    

    This assumes your TableCells should always be in "editing state". If this is not the case, you need to implement the state change in the startEdit/cancelEdit and commitEdit methods.