2

I have TableView with column inside it that must only accept numbers. and I added onMouseClickListener to enter edit mode on the mouse click instead of double click on the cell I want a way to not allowing the user to enter any character except numbers. My code is:

Callback<TableColumn<DailyDetails, String>, TableCell<DailyDetails, String>> defaultCellFactory
            = TextFieldTableCell.<DailyDetails>forTableColumn();
dailyCredit.setCellFactory(column -> {
        TableCell<DailyDetails, String> cell = defaultCellFactory.call(column);

        cell.setOnMouseClicked(e -> {
            if (!cell.isEditing() && !cell.isEmpty()) {
                cell.getTableView().edit(cell.getIndex(), column);
            }
        });
        return cell;
    });

I implemented Table cell from the scratch:

class NumberCell extends TableCell<DailyDetails, String> {

    private TextField textField;

    public NumberCell() {
    }

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

        if (textField == null) {
            createTextField();
        }

        setGraphic(textField);
        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        textField.selectAll();
    }

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

        setText(String.valueOf(getItem()));
        setContentDisplay(ContentDisplay.TEXT_ONLY);
    }

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

        if (empty) {
            setText(null);
            setGraphic(null);
        } else {
            if (isEditing()) {
                if (textField != null) {
                    textField.setText(getString());
                }
                setGraphic(textField);
                setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
            } else {
                setText(getString());
                setContentDisplay(ContentDisplay.TEXT_ONLY);
            }
        }
    }

    private void createTextField() {
        textField = new TextField(getString());
        //textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
        textField.lengthProperty().addListener(new ChangeListener<Number>(){
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
      if (newValue.intValue() > oldValue.intValue()) {
          char ch = textField.getText().charAt(oldValue.intValue());
          // Check if the new character is the number or other's
          if (!(ch >= '0' && ch <= '9' )) {
               // if it's not number then just setText to previous one
               textField.setText(textField.getText().substring(0,textField.getText().length()-1)); 
          }
     }
}

});
    }

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


Callback<TableColumn<DailyDetails, String>, 
        TableCell<DailyDetails, String>> cellFactory
            = (TableColumn<DailyDetails, String> p) -> new NumberCell();
    dailyDebit.setCellFactory(cellFactory);

the problem is i lost the on mouse listener cell.setOnMouseClicked!!! how do i get the cell again to assign the listener ???

bercik
  • 164
  • 12
Hassan Kbbewar
  • 183
  • 2
  • 4
  • 14
  • You will need to implement a `TableCell` from scratch that includes a `TextField` that only accepts numeric characters. See [this blog](http://fxexperience.com/2012/02/restricting-input-on-a-textfield/) for restricting input on a text field; the usual tutorials show you how to implement a table cell. Try to combine those and post a specific question if you get stuck. – James_D Apr 19 '15 at 02:09
  • thanks @James_D i implemented Table cell from the scratch : the problem is i lost the on mouse listener cell.setOnMouseClicked!!! how do i get the cell again to assign the listener ??? – Hassan Kbbewar Apr 19 '15 at 02:28
  • Try putting the mouse listener you previously had (`cell.setOnMouseClicked(...)`) in the constructor of your `NumberCell` class (`this.setOnMouseClicked(...)`). – James_D Apr 19 '15 at 02:48
  • sorry @James_D it doesn't work – Hassan Kbbewar Apr 19 '15 at 02:55
  • 1
    @James_D: isn't the new way to go a TextFormatter which got introduced with Java8u40? Unfortunately there's hardly any documentation about its usage. – Roland Apr 19 '15 at 05:16
  • Ah, yes, I keep forgetting about that. – James_D Apr 19 '15 at 12:54

2 Answers2

3

Just for driving the new api into everybody's brain: a full example with a slightly different TextFormatter (than in the other answer) that is Locale-aware and (dirtily!) hooked into core TextFieldTableCell, can be used in any custom editing TableCell as well:

/**
 * Example of how-to use a TextFormatter in a editing TableCell.
 */
public class CellFormatting extends Application {

    private Parent getContent() {
        ObservableList<IntData> data = FXCollections.observableArrayList(
                new IntData(1), new IntData(2), new IntData(3)
                );
        TableView<IntData> table = new TableView<>(data);
        table.setEditable(true);
        TableColumn<IntData, Integer> column = new TableColumn<>("Data");
        column.setCellValueFactory(new PropertyValueFactory("data"));
        // core default: will throw exception on illegal values
        // column.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));

        NumberFormat format = NumberFormat.getIntegerInstance();
        UnaryOperator<TextFormatter.Change> filter = c -> {
            if (c.isContentChange()) {
                ParsePosition parsePosition = new ParsePosition(0);
                // NumberFormat evaluates the beginning of the text
                format.parse(c.getControlNewText(), parsePosition);
                if (parsePosition.getIndex() == 0 ||
                        parsePosition.getIndex() < c.getControlNewText().length()) {
                    // reject parsing the complete text failed
                    return null;
                }
            }
            return c;
        };
       column.setCellFactory(c -> new ValidatingTextFieldTableCell<>(
            // note: each cell needs its own formatter
            // see comment by @SurprisedCoconut   
            new TextFormatter<Integer>(
            // note: should use local-aware converter instead of core!
            new IntegerStringConverter(), 0,
            filter)));
        table.getColumns().add(column);
        VBox box = new VBox(table);
        return box;
    }

    /**
     * TextFieldTableCell that validates input with a TextFormatter.
     * <p>
     * Extends TextFieldTableCell, accesses super's private field reflectively.
     * 
     */
    public static class ValidatingTextFieldTableCell<S, T> extends TextFieldTableCell<S, T> {

        private TextFormatter<T> formatter;
        private TextField textAlias;

        public ValidatingTextFieldTableCell() {
            this((StringConverter<T>)null);
        }

        public ValidatingTextFieldTableCell(StringConverter<T> converter) {
            super(converter);
        }

        public ValidatingTextFieldTableCell(TextFormatter<T> formatter) {
            super(formatter.getValueConverter());
            this.formatter = formatter;
        }

        /**
         * Overridden to install the formatter. <p>
         * 
         * Beware: implementation detail! super creates and configures
         * the textField lazy on first access, so have to install after
         * calling super.
         */
        @Override
        public void startEdit() {
            super.startEdit();
            installFormatter();
        }

        private void installFormatter() {
            if (formatter != null && isEditing() && textAlias == null) {
                textAlias = invokeTextField();
                textAlias.setTextFormatter(formatter);
            }
        }

        private TextField invokeTextField() {
            Class<?> clazz = TextFieldTableCell.class;
            try {
                Field field = clazz.getDeclaredField("textField");
                field.setAccessible(true);
                return (TextField) field.get(this);
            } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
                e.printStackTrace();
            }
            return null;
        }


    }

    public static class IntData {
        IntegerProperty data = new SimpleIntegerProperty(this, "data");
        public IntData(int value) {
            setData(value);
        }

        public void setData(int value) {
            data.set(value);
        }

        public int getData() {
            return data.get();
        }

        public IntegerProperty dataProperty() {
            return data;
        }
    }
    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setScene(new Scene(getContent()));
        primaryStage.show();
    }

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

BTW, the formatter is re-used from another question where the task at hand was to restrict input into a Spinner.

Community
  • 1
  • 1
kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • This code throws a `java.lang.IllegalStateException: Formatter is already used in other control` when switching edit mode between cells plus accepts all characters after that exception. Do you get the same result? – FuryFart Apr 20 '15 at 10:02
  • yes, seeing that as well - fixed (a formatter has to be created inside the factory) Thanks for the head up! – kleopatra Apr 20 '15 at 10:40
  • `parsePosition.getIndex() == 0` seems unnecessary as after this You anyway check `parsePosition.getIndex() < c.getControlNewText().length()`. Or is some kind of 'optimisation'? – kcpr Dec 27 '15 at 00:16
1

Use a TextFormatter on the TextField like this:

TextFormatter<String> formatter = new TextFormatter<String>( change -> {
    change.setText(change.getText().replaceAll("[^0-9.,]", ""));
    return change; 

});
textField.setTextFormatter(formatter);

Works with Java8u40 upwards. Use e. g. the TableView example from the Oracle site as base.

Roland
  • 18,114
  • 12
  • 62
  • 93