Strict 24-hour time in JFormattedTextField
Asked Answered
I

2

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);
    }
Iso answered 15/6, 2012 at 13:7 Comment(4)
The easy workaround is the use of JTextField, and using DocumentFilter :-) For DocumentFilter you can check this exampleSupramolecular
not true, could be 02:22 or 22:02Emblaze
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.Churchlike
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 :-)Supramolecular
C
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);
  }
}
Churchlike answered 15/6, 2012 at 13:37 Comment(7)
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 reusedIndemnification
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.Iso
@Indemnification good point, this could probably be changed to wrap a general formatter to do the same checking.Churchlike
@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.Churchlike
@Churchlike when returning null you should probably set the error index in the ParsePosition as wellIndemnification
@Indemnification 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?Churchlike
@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.Electromotor
N
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("");
        }
    });
Nena answered 15/6, 2012 at 13:26 Comment(3)
I will say this approach can stand the test of time :-)Supramolecular
@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 AlexIndemnification
@Indemnification 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.Nena

© 2022 - 2024 — McMap. All rights reserved.