trouble with deleting in masks in a JFormattedTextField
Asked Answered
B

2

6

I am having trouble with masks in a JFormattedTextField

I understand that it replaces invalid characters with a space, or whatever you define via setPlaceholderCharacter, but what I need it to do is allow deletion or backspace, and NOT insert a space in place of the character I deleted as long as the rest of the string is allowed in the mask.

For example, with the mask: *#*****, the string "12 abc" is valid.
If you put your cursor between the b and c characters, and press the backspace button, I need it to delete the b, resulting in "12 ac". Instead, it deletes it, and adds a space, becoming: "12 a c".

A simple code example is below to demonstrate.

I would appreciate any thoughts or examples to get around this issue.


public class testFrame extends javax.swing.JFrame {

    public testFrame() {

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        getContentPane().setLayout(new java.awt.FlowLayout());

        setMinimumSize(new Dimension(300,150));

        java.awt.Button closeButton = new java.awt.Button();
        JFormattedTextField maskTextField = new JFormattedTextField();
        maskTextField.setMinimumSize(new Dimension(100,30));

        getContentPane().add(maskTextField);

        closeButton.setLabel("close");
        closeButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                System.exit(0);
            }
        });

        getContentPane().add(closeButton);

        try {
            MaskFormatter someMask = new MaskFormatter("*#****");
            DefaultFormatterFactory formatterFactory 
                = new DefaultFormatterFactory(someMask);
            maskTextField.setFormatterFactory(formatterFactory);
        } catch (ParseException ex) {
            ex.printStackTrace();
        }
        maskTextField.setText("12 abc");

        pack();

    }

    public static void main(String args[]) {
        java.awt.EventQueue.invokeLater(new Runnable() {

            public void run() {
                new testFrame().setVisible(true);
            }
        });
    }
}

Updating code to reflect answer below. I added a second field so you can see the behaviour with and without the fix. Also a minor fix, I resized the windows and centred it in the screen to make it more friendly.

public class testFrame extends javax.swing.JFrame {

public testFrame() {
    setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
    setMinimumSize(new java.awt.Dimension(300, 200));
    getContentPane().setLayout(new java.awt.FlowLayout());


    JFormattedTextField maskTextField = new JFormattedTextField();
    maskTextField.setMinimumSize(new Dimension(100,30));
    getContentPane().add(maskTextField);


    JFormattedTextField maskTextField2 = new JFormattedTextField();
    maskTextField2.setMinimumSize(new Dimension(100,30));
    getContentPane().add(maskTextField2);

    java.awt.Button closeButton = new java.awt.Button();
    closeButton.setLabel("close");
    closeButton.addActionListener(new java.awt.event.ActionListener() {

        public void actionPerformed(java.awt.event.ActionEvent evt) {
            System.exit(0);
        }
    });

    getContentPane().add(closeButton);

    try {

        MaskFormatter someMask = new MaskFormatter("*#****");
        DefaultFormatterFactory formatterFactory = 
            new DefaultFormatterFactory(someMask);
        maskTextField.setFormatterFactory(formatterFactory);

        MaskFormatter someMask2 = new MaskFormatter("*#****");
        DefaultFormatterFactory formatterFactory2 = 
            new DefaultFormatterFactory(someMask2);
        maskTextField2.setFormatterFactory(formatterFactory2);

    } catch (ParseException ex) {
        ex.printStackTrace();
    }

    maskTextField.setText("12 abc");
    maskTextField2.setText("12 abc");

    // added per suggestion below
    if (maskTextField.getFormatter() instanceof DefaultFormatter) {
         DefaultFormatter f = (DefaultFormatter) maskTextField.getFormatter();
         f.setAllowsInvalid(true);

         // options are: 
         // JFormattedTextField.COMMIT
         // JFormattedTextField.COMMIT_OR_REVERT  --> default
         // JFormattedTextField.REVERT
         // JFormattedTextField.PERSIST
         maskTextField.setFocusLostBehavior(JFormattedTextField.PERSIST);
    } 
    pack();
    this.setLocationRelativeTo(null);

}

public static void main(String args[]) {
    java.awt.EventQueue.invokeLater(new Runnable() {

        public void run() {
            new testFrame().setVisible(true);
        }
    });
}

}

Burro answered 1/8, 2012 at 5:17 Comment(2)
sounds like a little misconception: a mask is something with a fixed lenght, each position filled either by a valid character or a placeHolder. On deleting a char, it is replaced by the placeHolder, in your example that's the default which is a space. So the first space is different from the second, semantically :-) Change the placeHolder to something else, to see that difference. As your requirement seems to be something like any-char-up-to-a-max-of-4-after-a-digit, you'll have to implement a custom formatter as @DuncanJones already suggestedAffiliate
I don't mind it being fixed length, and I guess I just have a special use case they didnt cater for. For example, if your mask was (###) ###-### and you were trying to type in (123) 456-7890 and accidentally typed in two 2s, i.e. (122) 345-6789, the standard mask behaviour seems to mean that if you delete the first 2, it would substitute a space (placeholder) giving you (12 ) 345-6789. I need it to delete the character and add the placeholder at the end of the string instead. But I guess this is not something others need so I will have to make a custom formatter :(Burro
A
5

Firstly, thank you for posting a decent working example.

It seems that the DefaultFormatter is the formatter used by your masked text field. I found that I could allow temporary invalid edits in the following manner:

if (maskTextField.getFormatter() instanceof DefaultFormatter) {
  DefaultFormatter f = (DefaultFormatter) maskTextField.getFormatter();
  f.setAllowsInvalid(true);          
}

Hopefully this enough of a pointer to get you started. Although note that this quick fix has the interesting behaviour of completely wiping the contents of the text field if you change focus while an invalid value is in the field. This seems contrary to the JavaDoc for JFormattedTextField which suggests that the default behaviour is COMMIT_OR_REVERT.

Abm answered 1/8, 2012 at 7:14 Comment(5)
I think this is definitely a great a hint. Only one thing to add, If you put in maskTextField.setFocusLostBehavior(JFormattedTextField.PERSIST); it seems to make it not clear the results. Thanks!Burro
testing some more, I realise this does not really solve the problem. Adding setAllowsInvalid(true) makes it ignore the mask, which is not what I want. What I am hoping for is IF the delete action does not invalidate the mask (by shifting the character that are to the right of the deleted characters) then deleting or backspacing should just do that, and not add a space. I guess if the delete action does invalidate the mask, it then should not work.Burro
If the mask is of a fixed length (presumably most/all are?), then wouldn't a delete always invalidate the mask? Either you temporarily permit breaking of the mask rules, or you will always end up with this odd space behaviour. Perhaps you could look into writing your own subclass of AbstractFormatter?Abm
Good question. Not always. If your mask is ###-***** and you delete a character after the hyphen, the mask would still be valid, as the empty character at the right (that would result from shifting) would be a space (or whatever you defined as the placeholder) and that is allowed by the *. But if the character is in the first 3 places, then yes, it would invalidate it, so I can see that using a placeholder would be better in that case. Which is why I want to have the validate code check the effect of the delete on the overall mask before blindly just adding a placeholder character.Burro
(The logic seems to be hard coded deep in the private canReplace(ReplaceHolder rh) method, within MaskFormatter, which is itself called by the private DefaultDocumentFilter, which is within DefaultFormatter).Burro
A
1

Just a thought - definitely as-is not suited for production and most probably not possible in the general case: you could try to wrap the default documentFilter and invoke custom checks/manipulations before/after calling the delegate.

Here's a snippet that seems to work for the particular example in your question:

public static class MyMaskFormatter extends MaskFormatter {

    DocumentFilter filter;

    /**
     * @param string
     * @throws ParseException
     */
    public MyMaskFormatter(String string) throws ParseException {
        super(string);
    }

    @Override
    protected DocumentFilter getDocumentFilter() {
        if (filter == null) {
            filter = new MyDocumentFilter(super.getDocumentFilter());
        }
        return filter;
    }

    public class MyDocumentFilter extends DocumentFilter {

        DocumentFilter delegate;

        MyDocumentFilter(DocumentFilter delegate) {
            this.delegate = delegate;
        }

        @Override
        public void remove(FilterBypass fb, int offset, int length)
                throws BadLocationException {
            String toRemove = fb.getDocument().getText(offset, length);
            delegate.remove(fb, offset, length);
            String replaced = fb.getDocument().getText(offset, length);
            if (replaced.charAt(0) == getPlaceholderCharacter() && 
                toRemove.charAt(0) != getPlaceholderCharacter()    ) {
                int sublength = fb.getDocument().getLength() - offset;
                String text = fb.getDocument().getText(offset, sublength);
                text = text.substring(1) + text.charAt(0);
                replace(fb, offset, sublength, text, null);
                getFormattedTextField().setCaretPosition(offset);
                //getNavigationFilter().setDot(fb, offset, null);
            }
        }

        @Override
        public void insertString(FilterBypass fb, int offset,
                String string, AttributeSet attr)
                throws BadLocationException {
            delegate.insertString(fb, offset, string, attr);
        }

        @Override
        public void replace(FilterBypass fb, int offset, int length,
                String text, AttributeSet attrs)
                throws BadLocationException {
            delegate.replace(fb, offset, length, text, attrs);
        }

    }

}
Affiliate answered 6/8, 2012 at 8:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.