JTextField, using Document Filter to filter integers and periods
Asked Answered
I

2

7

EDIT - added at then end of the post the answer we were able to achieve

This is my first post in SO, so i hope i can ask everything right!

I searched and didn't quite find a answer to my question despite similar questions being posted, so i hope this isn't a repost.

This is what a i got, a small application that uses JTextField to receive user's input and on top of that i have a DocumentFilter so the user can only input integers and a period in order to receive values that represent weight.

My problem is, with my DocumentFilter I'm not being able to filter "copy pasted" text and i can't filter a selected text removal.

Here is the code for the Filter

import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;

/**
* The Class IntAndDotFilter.
*/
public class IntAndDotFilter extends DocumentFilter {

/** The period counter. */
private int periodCounter = 0;

/** The number counter. */
private int numberCounter = 0;

private boolean canRemove = true;

public void setCanRemove(boolean canRemove) {
    this.canRemove = canRemove;
}

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

    if (periodCounter == 0) { // If there is no . on the text
        if (text.matches("\\.")) { // Checks if the input is a dot
            super.replace(fb, offset, length,
                    text.replaceAll("[^0-9.]", ""), attrs);
            periodCounter++; // If it is, inserts it and saves that info
        } else {
            super.replace(fb, offset, length,
                    text.replaceAll("[^0-9]", ""), attrs);
            // If not, checks if the input is a digit
            // and inserts if it is
        }
    } else { // If there is already a .
        if (text.matches("\\.")) { // Checks if the input is another .
            super.replace(fb, offset, length,
                    text.replaceAll("[^0-9]", ""), attrs);
            // If it is, filters so that cannot be more than one .
        } else {
            if (text.matches("[0-9]")) { // Checks if it's a digit
                if (numberCounter != 2) {
                    super.replace(fb, offset, length,
                            text.replaceAll("[^0-9]", ""), attrs);
                    numberCounter++;
                    // If yes, and if that is only the second one (0.00)
                    // inserts and
                    // saves the info that there are digits after the 1st .
                    // for removal purposes
                } else {
                    super.replace(fb, offset, length,
                            text.replaceAll(".", ""), attrs);
                    // if it is the third+ digit after . , doesn't allow the
                    // input
                }
            } else {
                super.replace(fb, offset, length, text.replaceAll(".", ""),
                        attrs);
                // Not being a digit, doesn't allow the
                // insertion of the given input
            }
        }
    }
}

@Override
public void remove(FilterBypass fb, int offset, int length)
        throws BadLocationException {

    if (canRemove) {
        if (periodCounter == 1) { // If there is a . in the text
            if (numberCounter != 0) { // and If there are digits after the .
                numberCounter--; // It means you'r removing a digit, so it
                                    // saves
                                    // that info
                super.remove(fb, offset, length); // And removes it
            } else { // If there are no digits it means you'r removing a .
                periodCounter--; // It saves that info allowing a new . to
                                    // be
                                    // inserted
                super.remove(fb, offset, length); // and removes it
            }
        } else { // If there is no . in the text there are no problems
            super.remove(fb, offset, length); // so it just removes whatever
                                                // there is (digit)
        }
    } else {

    }
}
}

the insertString method does the same has the replace method so i left it out, but in the application it's implemented.

Thanks in advance for your time!

EDIT - Plus it now has a filter to restrain the height input too

public class IntAndDotFilter extends DocumentFilter {

/** The Constant _maxCharacters. */
private static final int _maxCharacters = 10;

/** The _is weight. */
private Boolean _isWeight = null;


public IntAndDotFilter(Boolean isWeight) {
    super();
    _isWeight = isWeight;
}

public void replace(FilterBypass fb, int offset, int length, String string,
        AttributeSet attr) throws BadLocationException {

    String text = fb.getDocument().getText(0, fb.getDocument().getLength());
    text += string;

    if (_isWeight) {
        if ((fb.getDocument().getLength() + string.length() - length) <= _maxCharacters
                && text.matches("^[1]?[0-9]{1,2}([.][0-9]{0,2})?$")) {
            super.replace(fb, offset, length, string, attr);
        } else {
            Toolkit.getDefaultToolkit().beep();
        }
    } else {
        if ((fb.getDocument().getLength() + string.length() - length) <= _maxCharacters
                && text.matches("^([1]([.][0-9]{0,2})?)|([2]([.][0-5]?)?)$")) {
            super.replace(fb, offset, length, string, attr);
        } else {
            Toolkit.getDefaultToolkit().beep();
        }
    }
}

@Override
public void remove(FilterBypass fb, int offset, int length)
        throws BadLocationException {

    String text = fb.getDocument().getText(0, fb.getDocument().getLength());

    if (_isWeight) {
        if (text.matches("^[1]?[0-9]{1,2}([.][0-9]{0,2})?$")) {
            super.remove(fb, offset, length);
        } else {
            Toolkit.getDefaultToolkit().beep();
        }
    } else {
        if (text.matches("^([1]([.][0-9]{0,2})?)|([2]([.][0-5]?)?)$")) {
            super.remove(fb, offset, length);
        } else {
            Toolkit.getDefaultToolkit().beep();
        }
    }
}
Ibson answered 19/7, 2014 at 20:14 Comment(0)
H
13

You're making the filtering more complicated than it has to be. For inserting (if the code is same is replace), you are not able to enter probably because of the \\. check. You will only be able to paste a period, as that's what you are checking for. As for the remove, the suggestion below will apply.

To simplify things, you should just get the entire text of the document, then use regex to check if the entire document string matches the regex. It's much simpler than what you are trying to do. You can get a good explanation of the filtering process here.

Here's an example, using just insertString and replace. For the remove, it's no different, just get the text, and check if it matches a regex. I took part of the example from the answer in the link above. The scenario was that the OP wanted max charcters, along with only one decimal place allowed. That's what the regex matches. But could also match anything as you're typing or inserting such as 00 00. 00.0

import java.awt.GridBagLayout;
import java.awt.Toolkit;

import javax.swing.JFrame;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;

public class FilterDemo {

    public FilterDemo() {
        JFrame frame = new JFrame();
        frame.setLayout(new GridBagLayout());
        frame.setSize(300, 300);
        frame.add(createFilteredField());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLocationByPlatform(true);
        frame.setVisible(true);

    }

    public JTextField createFilteredField() {
        JTextField field = new JTextField();
        AbstractDocument document = (AbstractDocument) field.getDocument();
        final int maxCharacters = 10;
        document.setDocumentFilter(new DocumentFilter() {
            public void replace(FilterBypass fb, int offs, int length,
                    String str, AttributeSet a) throws BadLocationException {

                String text = fb.getDocument().getText(0,
                        fb.getDocument().getLength());
                text += str;
                if ((fb.getDocument().getLength() + str.length() - length) <= maxCharacters
                        && text.matches("^[0-9]+[.]?[0-9]{0,1}$")) {
                    super.replace(fb, offs, length, str, a);
                } else {
                    Toolkit.getDefaultToolkit().beep();
                }
            }

            public void insertString(FilterBypass fb, int offs, String str,
                    AttributeSet a) throws BadLocationException {

                String text = fb.getDocument().getText(0,
                        fb.getDocument().getLength());
                text += str;
                if ((fb.getDocument().getLength() + str.length()) <= maxCharacters
                        && text.matches("^[0-9]+[.]?[0-9]{0,1}$")) {
                    super.insertString(fb, offs, str, a);
                } else {
                    Toolkit.getDefaultToolkit().beep();
                }
            }
        });
        return field;
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new FilterDemo();
            }
        });
    }
}
Horacehoracio answered 20/7, 2014 at 3:41 Comment(5)
Really appreciated the help! Just had to do a few tweeks for my application, such has i wanted the weight have maximum xxx.xx and a different filter for height, like x.xx But now is totally working has intended! And yeah it was quite a messy code, thanks for the tip and response!Ibson
After some time searching and trying different approaches i'm not able to understand why does java don't let this pass: text.matches("^[0-9]{1,3}([.][0-9]{1,2})?$")) So basically i want the filter to filter numbers like 1 11 111 111. 111.1 111.11 And it just gets stuck after reading 11 I can't understand why...Ibson
It's the () around the last part. You're saying that the number must have at least number after the decimal. So you aren't able to add just a decimal. See how I separated the decimal and have it so it's either 0 or 1. I think my regex you should work if you just change the last 1 to 2, you should be able to put in normal currency values.Horacehoracio
Thanks for your response, finally i fixed it, basically really wanted the input to be restrict to 3 digits before the period at max and 2 digits after, so i came up with this wich works: "^[1]?[0-9]{1,2}([.][0-9]{0,2})?$" It has the [1]? so the entered weight be maxed at 199.99 And yeah it's working fine now, thanks for the help! I'll edit the original post with the answer, it can help other people =)Ibson
I am using the exact same DocumentFilter (IntAndDotFilter) provided in the above answer for my two JTextFields. I am able to get desired results in one text field, but not in the other. There is nothing displayed in the next JTextField. Also, is there is a decimal precision limit applied in this DocumentFilter? minMz = new JTextField(7); ((AbstractDocument)minMz.getDocument()).setDocumentFilter(new IntAndDotFilter(true)); maxMz = new JTextField(7); ((AbstractDocument)maxMz.getDocument()).setDocumentFilter(new IntAndDotFilter(true));Goar
S
1

This is a version of Paul Samsotha's answer with some bugs fixed, and the regular expression passed as an argument for more generic use. The original allowed insertion or replacement of an invalid character at the beginning of the input, or removal of a leading character that caused the field to become invalid (it didn't implement remove(). Once an invalid character was present, the input became locked.

This version should fix all those issues.

Sample usage:

  // Store the labels and textfields for later retrieval
  Vector<JLabel> jLabels = new Vector<JLabel>(8);
  Vector<JTextField> jTextFields = new Vector<JTextField>(8);

  //Create and populate the panel.
  Container contentPane = getContentPane();

  // Wrap everything in a BorderLayout
  JPanel borderPanel = new JPanel(new BorderLayout());
  contentPane.add(borderPanel);

  // Put the input fields in the CENTER position
  JPanel fieldPanel = new JPanel(new SpringLayout());
  borderPanel.add(fieldPanel, BorderLayout.CENTER);

  // Put the input fields in the CENTER position
  JPanel fieldPanel = new JPanel(new SpringLayout());
  borderPanel.add(fieldPanel, BorderLayout.CENTER);

  String[] labels = {"Player Name: ", "Initial Chips: "};
  String[] filters = {"^[A-Za-z][A-Za-z0-9_ ]*$", "^[1-9][0-9]*$"};
  final int numPairs = labels.length;

  // Create and associate the field inputs and labels and regex filters
  for (int i = 0; i < numPairs; i++) {
     JLabel label = new JLabel(labels[i], JLabel.TRAILING);
     fieldPanel.add(label);
     JTextField textField = createFilteredField(filters[i]);
     label.setLabelFor(textField);
     fieldPanel.add(textField);
     jLabels.add(label);
     jTextFields.add(textField);
  }

Code:

/* Filtered Text Field
 * @param   regex
 *          the regular expression to which this string is to be matched
 *
 * @return  {@code true} if, and only if, this string matches the
 *          given regular expression
 *
 * @throws  PatternSyntaxException
 *          if the regular expression's syntax is invalid
 */
public JTextField createFilteredField(String regex) {
   JTextField field = new JTextField(12);
   AbstractDocument document = (AbstractDocument) field.getDocument();
   final int maxCharacters = 20;
   document.setDocumentFilter(new DocumentFilter() {

      /**
       * Invoked prior to removal of the specified region in the
       * specified Document. Subclasses that want to conditionally allow
       * removal should override this and only call supers implementation as
       * necessary, or call directly into the <code>FilterBypass</code> as
       * necessary.
       *
       * @param fb FilterBypass that can be used to mutate Document
       * @param offset the offset from the beginning &gt;= 0
       * @param length the number of characters to remove &gt;= 0
       * @exception BadLocationException  some portion of the removal range
       *   was not a valid part of the document.  The location in the exception
       *   is the first bad position encountered.
       */
      public void remove(FilterBypass fb, int offset, int length) throws
            BadLocationException {
         String text = fb.getDocument().getText(0, fb.getDocument().getLength());
         String newText = text.substring(0, offset) + text.substring(offset + length);
         if (newText.matches(regex) || newText.length() == 0) {
            super.remove(fb, offset, length);
         }
      }

      /**
       * Invoked prior to replacing a region of text in the
       * specified Document. Subclasses that want to conditionally allow
       * replace should override this and only call supers implementation as
       * necessary, or call directly into the FilterBypass.
       *
       * @param fb FilterBypass that can be used to mutate Document
       * @param offset Location in Document
       * @param length Length of text to delete
       * @param _text Text to insert, null indicates no text to insert
       * @param attrs AttributeSet indicating attributes of inserted text,
       *              null is legal.
       * @exception BadLocationException  the given insert position is not a
       *   valid position within the document
       */
      public void replace(FilterBypass fb, int offset, int length,
                          String _text, AttributeSet attrs) throws BadLocationException {

         String text = fb.getDocument().getText(0, fb.getDocument().getLength());
         String newText = text.substring(0, offset) + _text + text.substring(offset + length);
         if (newText.length() <= maxCharacters && newText.matches(regex)) {
            super.replace(fb, offset, length, _text, attrs);
         } else {
            Toolkit.getDefaultToolkit().beep();
         }
      }

      /**
       * Invoked prior to insertion of text into the
       * specified Document. Subclasses that want to conditionally allow
       * insertion should override this and only call supers implementation as
       * necessary, or call directly into the FilterBypass.
       *
       * @param fb FilterBypass that can be used to mutate Document
       * @param offset  the offset into the document to insert the content &gt;= 0.
       *    All positions that track change at or after the given location
       *    will move.
       * @param string the string to insert
       * @param attr      the attributes to associate with the inserted
       *   content.  This may be null if there are no attributes.
       * @exception BadLocationException  the given insert position is not a
       *   valid position within the document
       */
      public void insertString(FilterBypass fb, int offset, String string,
                               AttributeSet attr) throws BadLocationException {

         String text = fb.getDocument().getText(0, fb.getDocument().getLength());
         String newText = text.substring(0, offset) + string + text.substring(offset);
         if ((fb.getDocument().getLength() + string.length()) <= maxCharacters
               && newText.matches(regex)) {
            super.insertString(fb, offset, string, attr);
         } else {
            Toolkit.getDefaultToolkit().beep();
         }
      }
   });
   return field;
}
Splendor answered 19/9, 2020 at 0:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.