Andy Dingfelder
Andy Dingfelder

Reputation: 2140

trouble with deleting in masks in a JFormattedTextField

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);
        }
    });
}

}

Upvotes: 6

Views: 2246

Answers (2)

kleopatra
kleopatra

Reputation: 51524

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);
        }

    }

}

Upvotes: 1

Duncan Jones
Duncan Jones

Reputation: 69329

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.

Upvotes: 5

Related Questions