Search code examples
javajavafx

JavaFX TextFormatter with input validation for a lower limit integer value


I am trying to make TableView in which i can enter only integer values that too within range say 100 to 500. Anything different from this range should be discarded. I did implement it using TextFormatter but i got stuck in for lower limit.

        UnaryOperator<Change> flowFilter = change -> {
        String controlNewText =  change.getControlNewText();
        String text = change.getText();
        
        System.out.println("controlNetText : "+controlNewText);
        System.out.println("text : "+text);
         
        if(change.getControlNewText().isEmpty()) {
            System.out.println("empty....");
             return change;
         } else if (controlNewText.matches("\\d*(\\.\\d*)?")) {
             int val = Integer.parseInt(controlNewText);

             //this below logic is failing at 
             // val >= MIN_RANGE_VALUE
             if( val >= MIN_RANGE_VALUE && val <= MAX_RANGE_VALUE) {
                 System.out.println("min max value .....");
                 return change;
             } 
         }
        return null;
    };

Solution

  • I got the answer or maybe something which works fine for me. Let me explain what I wanted. I wanted to create a TableView with features of committing the editing TableCell when focused is lost. I used this link to have my own custom TableCell.

    Also, I needed that user can input only values in a particular range say 100 to 500. I used JavaFX TextFormatter class for that. With the help of TextFormatter, I was able to filter user input so that only Integer or Float values are allowed. I was also able to put upper range limit (here 500) with the help of TextFormatter. The real issue came with lower range limit because you cannot predict users mind. Say, if user types "12" and our range is 100 - 500, then I cannot predict whether user will commit it or will type more. This was the real problem. This lower range limit problem cannot be solved with TextFormatter.

    A comment suggested to put the checking logic in commitEdit() method. I did it and it worked for me. What the below code does is that if user enters any value within range 100-500, it is accepted. Otherwise the recent old committed value remain in TableCell of the TableView. Below is the code for Custom TableCell:

    class CustomTableCell<S, T> extends TableCell<S, T> {
        private final TextField textField = new TextField();
        private final StringConverter<T> converter;
        private final TextFormatter<T> textFormatter;
    
        int MIN_RANGE_VALUE = 100;
        int MAX_RANGE_VALUE = 500;
        int MAX_DIGITS = 3;
    
        public CustomTableCell(StringConverter<T> converter) {
            this.converter = converter;
    
            itemProperty().addListener((obsVal, oldItem, newItem) -> {
                if(newItem == null) {
                    setText(null);
                } else {
                    setText(converter.toString(newItem));    
                }
            });
            setGraphic(textField);    
            setContentDisplay(ContentDisplay.TEXT_ONLY);
    
            textFormatter = new TextFormatter<>(flowFilter);
    
            textField.setTextFormatter(new TextFormatter<>(flowFilter));
    
            textField.setOnAction(event -> {
                System.out.println("textField setOnAction called....");
                commitEdit(this.converter.fromString(textField.getText()));
            });
    
            textField.focusedProperty().addListener((obsVal, wasFocused, isNowFocused) -> {
                if(!isNowFocused) {
                    System.out.println("focused lost.....");   
                    System.out.println("textField.getText() : " +textField.getText());
                    commitEdit(this.converter.fromString(textField.getText()));
                    
                   
                }
            });
    
            textField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
                if(event.getCode() == KeyCode.ESCAPE) {
                    textField.setText(converter.toString(getItem()));
                    cancelEdit();
                    event.consume();
                } else if(event.getCode() == KeyCode.RIGHT) {
                    getTableView().getSelectionModel().selectRightCell();
                    event.consume();
                } else if(event.getCode() == KeyCode.LEFT) {
                    getTableView().getSelectionModel().selectLeftCell();
                    event.consume();
                } else if(event.getCode() == KeyCode.UP) {
                    getTableView().getSelectionModel().selectAboveCell();
                    event.consume();
                } else if(event.getCode() == KeyCode.DOWN) {
                    getTableView().getSelectionModel().selectBelowCell();
                    event.consume();
                }
            });
        }
    
        UnaryOperator<Change> flowFilter = change -> {
            String controlNewText =  change.getControlNewText();
            String text = change.getText();
            
            System.out.println("controlNetText : "+controlNewText);
            System.out.println("text : "+text);
             
            if(change.getControlNewText().isEmpty()) {
                System.out.println("empty returned....");
                return change;
            } else if (controlNewText.matches("\\d*") && controlNewText.length() <= MAX_DIGITS) {
                 int val = Integer.parseInt(controlNewText);
                 if( val <= MAX_RANGE_VALUE) {
                     System.out.println("max min range returned...");
                     return change;
                 } 
            }
            System.out.println("textFormatter null returned....");
            return null;
        };
    
        public static final StringConverter<String> IDENTITY_CONVERTER = new StringConverter<String>() {
            @Override
            public String toString(String object) {
               return object;
            }
    
            @Override
            public String fromString(String string) {
               return string;
            }
    
        };
    
        //Convenience method for creating an EditCell for a String value
        public static CustomTableCell<ReceipeDataModel, Number> createStringCustomTableCell() {
            return new CustomTableCell<ReceipeDataModel, Number>(new NumberStringConverter());
        }
    
        //set the text of TextField and display graphic
        @Override
        public void startEdit() {
            super.startEdit();
            textField.setText(converter.toString(getItem()));
            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
            textField.requestFocus();
        }
    
        //revert to text display
        @Override
        public void cancelEdit() {
            super.cancelEdit();
            setContentDisplay(ContentDisplay.TEXT_ONLY);
        }
    
        //commits the edit. Update the property if possible and revert to text display
        @Override
        public void commitEdit(T item) {
            //below code for empty string and converter returns null
            if(item == null) {
                item = getItem();
            }
    
            //below code for putting range limits
            if(item != null) {
                long val = (long)item;  
                System.out.println("inside min max range if.....");
                item = (val >= MIN_RANGE_VALUE && val <= MAX_RANGE_VALUE) ? item : getItem();
            }
    
            //this block is necessary because by deafult mechanism return false for isEditng() method when focus is lost. 
            //By Default, Only when we click on same TableRow, does the lost focus commits, not when we click outside the tableRow
            if(item != null && ! isEditing() && !item.equals(getItem())) {
                TableView<S> table = getTableView();
                if(table != null) {
                    TableColumn<S, T> col = getTableColumn();
                    TablePosition<S, T> pos = new TablePosition<>(table, getIndex(), col);
                    CellEditEvent<S, T> cellEditEvent = new CellEditEvent<>(table, pos, TableColumn.editCommitEvent(), item);
                    Event.fireEvent(col, cellEditEvent);
                }
            }
    
            super.commitEdit(item);
    
            setContentDisplay(ContentDisplay.TEXT_ONLY);
        }
    }