Numeric TextField for Integers in JavaFX 8 with TextFormatter and/or UnaryOperator

28,689

Solution 1

The converter is different to the filter: the converter specifies how to convert the text to a value, and the filter filters changes the user may make. It sounds like here you want both, but you want the filter to more accurately filter the changes that are allowed.

I usually find it easiest to check the new value of the text if the change were accepted. You want to optionally have a -, followed by 1-9 with any number of digits after it. It's important to allow an empty string, else the user won't be able to delete everything.

So you probably need something like

UnaryOperator<Change> integerFilter = change -> {
    String newText = change.getControlNewText();
    if (newText.matches("-?([1-9][0-9]*)?")) { 
        return change;
    }
    return null;
};

myNumericField.setTextFormatter(
    new TextFormatter<Integer>(new IntegerStringConverter(), 0, integerFilter));

You can even add more functionality to the filter to let it process - in a smarter way, e.g.

UnaryOperator<Change> integerFilter = change -> {
    String newText = change.getControlNewText();
    // if proposed change results in a valid value, return change as-is:
    if (newText.matches("-?([1-9][0-9]*)?")) { 
        return change;
    } else if ("-".equals(change.getText()) ) {

        // if user types or pastes a "-" in middle of current text,
        // toggle sign of value:

        if (change.getControlText().startsWith("-")) {
            // if we currently start with a "-", remove first character:
            change.setText("");
            change.setRange(0, 1);
            // since we're deleting a character instead of adding one,
            // the caret position needs to move back one, instead of 
            // moving forward one, so we modify the proposed change to
            // move the caret two places earlier than the proposed change:
            change.setCaretPosition(change.getCaretPosition()-2);
            change.setAnchor(change.getAnchor()-2);
        } else {
            // otherwise just insert at the beginning of the text:
            change.setRange(0, 0);
        }
        return change ;
    }
    // invalid change, veto it by returning null:
    return null;
};

This will let the user press - at any point and it will toggle the sign of the integer.

SSCCE:

import java.util.function.UnaryOperator;

import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextFormatter.Change;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import javafx.util.converter.IntegerStringConverter;

public class IntegerFieldExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        TextField integerField = new TextField();
        UnaryOperator<Change> integerFilter = change -> {
            String newText = change.getControlNewText();
            if (newText.matches("-?([1-9][0-9]*)?")) { 
                return change;
            } else if ("-".equals(change.getText()) ) {
                if (change.getControlText().startsWith("-")) {
                    change.setText("");
                    change.setRange(0, 1);
                    change.setCaretPosition(change.getCaretPosition()-2);
                    change.setAnchor(change.getAnchor()-2);
                    return change ;
                } else {
                    change.setRange(0, 0);
                    return change ;
                }
            }
            return null;
        };

        // modified version of standard converter that evaluates an empty string 
        // as zero instead of null:
        StringConverter<Integer> converter = new IntegerStringConverter() {
            @Override
            public Integer fromString(String s) {
                if (s.isEmpty()) return 0 ;
                return super.fromString(s);
            }
        };

        TextFormatter<Integer> textFormatter = 
                new TextFormatter<Integer>(converter, 0, integerFilter);
        integerField.setTextFormatter(textFormatter);

        // demo listener:
        textFormatter.valueProperty().addListener((obs, oldValue, newValue) -> System.out.println(newValue));

        VBox root = new VBox(5, integerField, new Button("Click Me"));
        root.setAlignment(Pos.CENTER);
        Scene scene = new Scene(root, 300, 120);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

Solution 2

Is there a way to enforce the conversion of the second method with the IntegerStringConverter every time the value of the TextField is updated?

Although the answer by @James_D gives you what you want, I would like to add a different perspective also. Your idea is to hold the hand of the user, with every single keypress. This can be helpful, but it can also be frustrating to the user. Actions like copy pasting into the text field, or editing an existing input at different positions, do not work well with the hand-holding approach.

A reason why you might want to apply the conversion/filtering immediately is because the user may not be aware of the input being invalid, and perhaps missing the correction when tabbing to the next field. So how about instead of restricting what the user can input, you visualize whether the current input is valid or not, without changing the text contents. So for instance, you could add a red border to the text field while the contents are invalid. You could still use a StringConverter in addition to this.

For example

myNumericField.setTextFormatter(new TextFormatter<>(new IntegerStringConverter()));
myNumericField.textProperty().addListener((obs,oldv,newv) -> {
    try {
        myNumericField.getTextFormatter().getValueConverter().fromString(newv);
        // no exception above means valid
        myNumericField.setBorder(null);
    } catch (NumberFormatException e) {
        myNumericField.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.SOLID, new CornerRadii(3), new BorderWidths(2), new Insets(-2))));
    }
});

text field with red border

The converter can also easily be extended to limit the valid number range.

Share:
28,689

Related videos on Youtube

ShadowEagle
Author by

ShadowEagle

Updated on July 09, 2022

Comments

  • ShadowEagle
    ShadowEagle almost 2 years

    I am trying to create a numeric TextField for Integers by using the TextFormatter of JavaFX 8.

    Solution with UnaryOperator:

    UnaryOperator<Change> integerFilter = change -> {
        String input = change.getText();
        if (input.matches("[0-9]*")) { 
            return change;
        }
        return null;
    };
    
    myNumericField.setTextFormatter(new TextFormatter<String>(integerFilter));
    

    Solution with IntegerStringConverter:

    myNumericField.setTextFormatter(new TextFormatter<>(new IntegerStringConverter()));  
    

    Both solutions have their own problems. With the UnaryOperator, I can only enter digits from 0 to 9 like intended, but I also need to enter negative values like "-512", where the sign is only allowed at the first position. Also I don't want numbers like "00016" which is still possible.

    The IntegerStringConverter method works way better: Every invalid number like "-16-123" is not accepted and numbers like "0123" get converted to "123". But the conversion only happens when the text is commited (via pressing enter) or when the TextField loses its focus.

    Is there a way to enforce the conversion of the second method with the IntegerStringConverter every time the value of the TextField is updated?

  • flakes
    flakes over 7 years
    Smart use of ? on the number group! Was thinking about -?([1-9]?|[1-9][0-9]*), but this is much cleaner!
  • ShadowEagle
    ShadowEagle over 7 years
    I really appreciate your effort, your solution worked perfectly!
  • mkl
    mkl almost 7 years
    On stack overflow some words of explanation / usage instruction usually are welcomed.
  • kleopatra
    kleopatra almost 3 years
    since fx8u80 this is wrong (you must not change the property you are listening to), use a textFormatter instead
  • r-uu
    r-uu over 2 years
    imo this is a very elegant solution: no regex, clean separation between formatting, conversion and validation (the latter not being shown in this example but easy to extend). Thanks for this!