Text field validation in JavaFX


Question

In a text field i want to perform a operation in the following way:

-> a default text 00:00:00 is set for time as the text field is for time and the character length of the field is 8.

-> if the cursor is in position 0(zero) and if i press a number key it should replace the position 0 value and move the cursor to next position.

   eg : 00:00:00
        20:00:00

explanation : now the cursor z in position 0 and i press the key 2 it should replace the value 0 in position 0 and move to position 1

when i press any non-numeric key it should not replace any of the value but the cursor should move to next position.

1
2
6/19/2013 10:50:12 AM

Accepted Answer

This works perfectly :

    import java.util.regex.Pattern;  
    import javafx.application.Application;  
    import javafx.beans.binding.Bindings;  
    import javafx.beans.binding.IntegerBinding;  
    import javafx.beans.property.ReadOnlyIntegerProperty;  
    import javafx.beans.property.ReadOnlyIntegerWrapper;  
    import javafx.geometry.Insets;  
    import javafx.scene.Scene;  
    import javafx.scene.control.IndexRange;  
    import javafx.scene.control.Label;  

import javafx.scene.control.TextField;  
import javafx.scene.layout.VBox;  
import javafx.stage.Stage;  
public class TimeTextFieldTest extends Application {  
 @Override  
  public void start(Stage primaryStage) {  
  VBox root = new VBox(5);  
  root.setPadding(new Insets(5));  
  Label hrLabel = new Label();  
  Label minLabel = new Label();  
  Label secLabel = new Label();  
  TimeTextField timeTextField = new TimeTextField();  
  hrLabel.textProperty().bind(Bindings.format("Hours: %d", timeTextField.hoursProperty()));  
  minLabel.textProperty().bind(Bindings.format("Minutes: %d", timeTextField.minutesProperty()));  
  secLabel.textProperty().bind(Bindings.format("Seconds: %d", timeTextField.secondsProperty()));  
  root.getChildren().addAll(timeTextField, hrLabel, minLabel, secLabel);  
  Scene scene = new Scene(root);  
  primaryStage.setScene(scene);  
  primaryStage.show();  
  }  
  public static void main(String[] args) {  
  launch(args);  
  }  
  public static class TimeTextField extends TextField {  

    enum Unit {HOURS, MINUTES, SECONDS};  
    private final Pattern timePattern ;  
    private final ReadOnlyIntegerWrapper hours ;  
    private final ReadOnlyIntegerWrapper minutes ;  
    private final ReadOnlyIntegerWrapper seconds ;  

    public TimeTextField() {  
      this("00:00:00");  
    }  
    public TimeTextField(String time) {  
      super(time);  
      timePattern = Pattern.compile("\\d\\d:\\d\\d:\\d\\d");  
      if (! validate(time)) {  
        throw new IllegalArgumentException("Invalid time: "+time);  
      }  
      hours = new ReadOnlyIntegerWrapper(this, "hours");  
      minutes = new ReadOnlyIntegerWrapper(this, "minutes");  
      seconds = new ReadOnlyIntegerWrapper(this, "seconds");  
      hours.bind(new TimeTextField.TimeUnitBinding(Unit.HOURS));  
      minutes.bind(new TimeTextField.TimeUnitBinding(Unit.MINUTES));  
      seconds.bind(new TimeTextField.TimeUnitBinding(Unit.SECONDS));  
    }  

    public ReadOnlyIntegerProperty hoursProperty() {  
      return hours.getReadOnlyProperty();  
    }  

    public int getHours() {  
      return hours.get() ;  
    }  

    public ReadOnlyIntegerProperty minutesProperty() {  
      return minutes.getReadOnlyProperty();  
    }  

    public int getMinutes() {  
      return minutes.get();  
    }  

    public ReadOnlyIntegerProperty secondsProperty() {  
      return seconds.getReadOnlyProperty();  
    }  

    public int getSeconds() {  
      return seconds.get();  
    }  

    @Override  
    public void appendText(String text) {  
      // Ignore this. Our text is always 8 characters long, we cannot append anything  
    }  

    @Override  
    public boolean deleteNextChar() {  
      boolean success = false ;  

      // If there's a selection, delete it:  
      final IndexRange selection = getSelection();  
      if (selection.getLength()>0) {  
        int selectionEnd = selection.getEnd();  
        this.deleteText(selection);  
        this.positionCaret(selectionEnd);  
        success = true ;  
      } else {  
        // If the caret preceeds a digit, replace that digit with a zero and move the caret forward. Else just move the caret forward.  
      int caret = this.getCaretPosition();  
      if (caret % 3 != 2) { // not preceeding a colon  
        String currentText = this.getText();  
        setText(currentText.substring(0, caret) + "0" + currentText.substring(caret+1));  
        success = true ;  
      }  
      this.positionCaret(Math.min(caret+1, this.getText().length()));  
      }  
      return success ;  
    }  

    @Override  
    public boolean deletePreviousChar() {  
      boolean success = false ;  
      // If there's a selection, delete it:  
      final IndexRange selection = getSelection();  
      if (selection.getLength()>0) {  
        int selectionStart = selection.getStart();  
        this.deleteText(selection);  
        this.positionCaret(selectionStart);  
        success = true ;  
      } else {  
      // If the caret is after a digit, replace that digit with a zero and move the caret backward. Else just move the caret back.  
        int caret = this.getCaretPosition();  
        if (caret % 3 != 0) { // not following a colon  
          String currentText = this.getText();  
          setText(currentText.substring(0, caret-1) + "0" + currentText.substring(caret));  
          success = true ;  
        }  
        this.positionCaret(Math.max(caret-1, 0));  
      }  
      return success ;  
    }  

    @Override  
    public void deleteText(IndexRange range) {  
      this.deleteText(range.getStart(), range.getEnd());  
    }  

    @Override  
    public void deleteText(int begin, int end) {  
      // Replace all digits in the given range with zero:  
      StringBuilder builder = new StringBuilder(this.getText());  
      for (int c = begin; c<end; c++) {  
        if (c % 3 != 2) { // Not at a colon:  
          builder.replace(c, c+1, "0");  
        }  
      }  
      this.setText(builder.toString());  
    }  

    @Override  
    public void insertText(int index, String text) {  
      // Handle an insert by replacing the range from index to index+text.length() with text, if that results in a valid string:  
      StringBuilder builder = new StringBuilder(this.getText());  
      builder.replace(index, index+text.length(), text);  
      final String testText = builder.toString();  
      if (validate(testText)) {  
        this.setText(testText);  
      }  
      this.positionCaret(index + text.length());  
    }  

    @Override  
    public void replaceSelection(String replacement) {  
      final IndexRange selection = this.getSelection();  
      if (selection.getLength()==0) {  
        this.insertText(selection.getStart(), replacement);  
      } else {  
        this.replaceText(selection.getStart(), selection.getEnd(), replacement);  
      }  
    }  

    @Override  
    public void replaceText(IndexRange range, String text) {  
      this.replaceText(range.getStart(), range.getEnd(), text);  
    }  

    @Override  
    public void replaceText(int begin, int end, String text) {  
      if (begin==end) {  
        this.insertText(begin, text);  
      } else {  
      // only handle this if text.length() is equal to the number of characters being replaced, and if the replacement results in a valid string:  
      if (text.length() == end - begin) {  
        StringBuilder builder = new StringBuilder(this.getText());  
        builder.replace(begin, end, text);  
        String testText = builder.toString();  
        if (validate(testText)) {  
          this.setText(testText);  
        }  
          this.positionCaret(end);  
      }  
      }  
    }  

    private boolean validate(String time) {  
      if (! timePattern.matcher(time).matches()) {  
        return false ;  
      }  
      String[] tokens = time.split(":");  
      assert tokens.length == 3 ;  
      try {  
        int hours = Integer.parseInt(tokens[0]);  
        int mins = Integer.parseInt(tokens[1]);  
        int secs = Integer.parseInt(tokens[2]);  
        if (hours < 0 || hours > 23) {  
          return false ;  
        }  
        if (mins < 0 || mins > 59) {  
          return false ;  
        }  
        if (secs < 0 || secs > 59) {  
          return false ;  
        }  
        return true ;  
      } catch (NumberFormatException nfe) {  
        // regex matching should assure we never reach this catch block  
        assert false ;  
        return false ;  
      }  
    }  

    private final class TimeUnitBinding extends IntegerBinding {  

      final Unit unit ;  
      TimeUnitBinding(Unit unit) {  
        this.bind(textProperty());  
        this.unit = unit ;  
      }  
      @Override  
      protected int computeValue() {  
        // Crazy enum magic  
        String token = getText().split(":")[unit.ordinal()];  
        return Integer.parseInt(token);  
      }  

    }  

  }  
} 
2
6/20/2013 2:47:38 PM

I would register an event listener for key typed events e.g.

myTextField.setOnKeyTyped( new EventListener<KeyEvent>() { ...handler_code... });

This listener will tell you what was just typed and you should have access to the text field in the listener so you can determine the caret position. That should be all the information you need.

For example if the user types a number delete the character to the right of the caret and if the user presses a non-numeric key delete the character to the left of the caret. Additional checking will be required to handle colons and the end of the string obviously.


Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon