6

I am trying to create a JFormattedTextField that only accepts a 24-hour time.

I am very close to a solution, but have one case where the following code example does not work.

If you enter the time "222" and change focus from the field, the time is corrected to "2202". I would like it to only accept a full 4 digit 24-hour time. This code works as I want in almost all cases, except the one I just mentioned. Any suggestions?

    public static void main(String[] args) throws ParseException {
        DateFormat dateFormat = new SimpleDateFormat("HHmm");
        dateFormat.setLenient(false);

        DateFormatter dateFormatter =  new DateFormatter(dateFormat);

        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JFormattedTextField textField = new JFormattedTextField(dateFormatter);
        frame.add(textField, BorderLayout.NORTH);

        frame.add(new JTextField("This is here so you can change focus."), BorderLayout.SOUTH);
        frame.setSize(250, 100);
        frame.setVisible(true);
    }
FuryComputers
  • 896
  • 2
  • 12
  • 23
  • The easy workaround is the use of `JTextField`, and using `DocumentFilter` :-) For [DocumentFilter](http://docs.oracle.com/javase/7/docs/api/javax/swing/text/DocumentFilter.html) you can check this [example](http://stackoverflow.com/questions/9477354/how-to-allow-introducing-only-digits-in-jtextfield/9478124#9478124) – nIcE cOw Jun 15 '12 at 13:16
  • not true, could be 02:22 or 22:02 – mKorbel Jun 15 '12 at 13:23
  • 4
    You might be out of luck if you're using SimpleDateFormat. From the Javadoc: "For parsing, the number of pattern letters is ignored unless it's needed to separate two adjacent fields." It parses the first two digits as the hour field, so it knows that the remaining digit must be the minutes field. – Alex Jun 15 '12 at 13:27
  • 1
    Ahha, too true, do one thing then on `FocusLost` check the length if it's less than 4, then present one `JOptionPane` for the user to have a look at, or don't let this component to lose focus if length is less than 4 :-) – nIcE cOw Jun 15 '12 at 13:30

2 Answers2

5

As others have mentioned, your best bet is probably to validate the lenght of the input string. My preferred approach would be subclassing SimpleDateFormat to keep all the parsing logic in one place:

public class LengthCheckingDateFormat extends SimpleDateFormat {

  public LengthCheckingDateFormat(String pattern) { super(pattern); }

  @Override
  public Date parse(String s, ParsePosition p) {
    if (s == null || (s.length() - p.getIndex()) < toPattern().length()) {
      p.setErrorIndex(p.getIndex());
      return null;
    }
    return super.parse(s, p);
  }
}
Alex
  • 13,811
  • 1
  • 37
  • 50
  • Or even more reusable: create a decorator for a standard Java format which only indicates parsing is succesfull when the whole input string could be parsed (a `ParseAllFormat`), so it can be reused – Robin Jun 15 '12 at 13:45
  • 1
    Great answer, I like overriding SimpleDateFormat rather than adding the focus listener. The one change I made to your code is rather than throwing a ParseException, I return null as per the SimpleDateFormat parse method JavaDoc. – FuryComputers Jun 15 '12 at 13:46
  • @Robin good point, this could probably be changed to wrap a general formatter to do the same checking. – Alex Jun 15 '12 at 13:58
  • @FuryComptuers good catch - single-arg parse throws an exception, multi-arg returns null. Changed the code to reflect that, and also to take the parse position into account when checking the length. – Alex Jun 15 '12 at 13:59
  • @Alex when returning `null` you should probably set the error index in the `ParsePosition` as well – Robin Jun 15 '12 at 14:00
  • @Robin done and done. I set it at the start position, you could probably make an argument to set it at the end of the string as well ("input ended prematurely"). So is there any way to get a meaningful error message back to the user in this situation? – Alex Jun 15 '12 at 14:05
  • @Alex: Great answer. Since you've written your own class, you could throw your own exception and it would be up to the UI developer to produce a meaningful error message. This would be overkill unless you were developing a group of date editing extensions. – Gilbert Le Blanc Jun 15 '12 at 15:12
2

I wonder since the DateFormatter appears to do 'almost' all the trick.

Therefore, why don't you Override its method one that does the final validation and formatting to treat all entered Strings that are too short as empty String.

        DateFormatter dateFormatter = new DateFormatter(dateFormat) {
            @Override
            public Object stringToValue(String text) throws ParseException {
                if(!getFormattedTextField().hasFocus())
                    if (text.length() != 4) {
                        return null;
                    }
                return super.stringToValue(text);
            }            
        };

EDIT:

As suggested by @nIcE cOw the easiest will be to serve it on focus lost:

    final JFormattedTextField textField = new JFormattedTextField(dateFormatter);
    textField.addFocusListener(new FocusAdapter() {
        @Override
        public void focusLost(FocusEvent e) {
            super.focusLost(e);
            if(textField.getText().length() != 4)
                textField.setText("");
        }
    });
Boro
  • 7,913
  • 4
  • 43
  • 85
  • I will say this approach can stand the test of time :-) – nIcE cOw Jun 15 '12 at 13:47
  • @nIcEcOw I would say this leads to unexpected behavior of the `JFormattedTextField`. Normally when the value is invalid it reverts to the previous valid value on `focusLost`. But here you are manually modifying the contents ... . And not sure what will happen when you press ENTER with an invalid value. So I would strongly suggest to go for the accepted answer posted by Alex – Robin Jun 15 '12 at 13:50
  • @Robin ENTER doesn't do anything by default. BTW: I found the method of `DateFormatter` that does the format I added a solution involving it to my answer, sadly it also relays on the focus of the text field as I wasn't sure how to detect the right time for calling the length test as the method `stringToValue` is called on any key enter. Putting it all together I would go with Alex 's answer myself (+1) as it is less hacky. – Boro Jun 15 '12 at 13:58