0

I’m trying to create JTextField that can accept only double (including scientific notation) with this check:

abstract class DoubleKeyAdapter extends KeyAdapter
{
    @Override
    public void KeyTyped(KeyEvent e)
    {
        if (!(((JTextField) e.getSource()).getText() + e.getKeyChar()).matches(“[+-]?\\d*(\\.\\d*)?([eE][+-]?\\d*)?”))
            e.consume();
    }

    @Override
    public abstract void KeyReleased(KeyEvent e);
}

The problem is when I try to add - for example to the beggining of the text field. it doesn’t let me do so because it does the checking by appending - at the end of the text, in other words, I can’t know where the new character had been added. So my questions are:

  1. Is there a way to get a preview of the entire text before it is present in the text field?
  2. Is there a way to create JTextField (or extention of it) that does it better?
  3. Is there a way to know the location of the new character?
Roy Ash
  • 1,078
  • 1
  • 11
  • 28

2 Answers2

2

You can use a DocumentFilter for this.

It allows you to do editing before the text is inserted into the Document.

import java.awt.*;
import javax.swing.*;
import javax.swing.text.*;

public class DoubleFilter extends DocumentFilter
{
    @Override
    public void insertString(FilterBypass fb, int offset, String text, AttributeSet attributes)
        throws BadLocationException
    {
        replace(fb, offset, 0, text, attributes);
    }

    @Override
    public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attributes)
        throws BadLocationException
    {
        //  In case someone tries to clear the Document by using setText(null)

        if (text == null)
            text = "";

        //  Build the text string assuming the replace of the text is successfull

        Document doc = fb.getDocument();
        StringBuilder sb = new StringBuilder();
        sb.append(doc.getText(0, doc.getLength()));
        sb.replace(offset, offset + length, text);

        if (validReplace(sb.toString()))
            super.replace(fb, offset, length, text, attributes);
        else
            Toolkit.getDefaultToolkit().beep();
    }

    private boolean validReplace(String text)
    {
        //  In case setText("") is used to clear the Document

        if (text.isEmpty())
            return true;

        //  Verify input is a Double

        try
        {
            Double.parseDouble( text );
            return true;
        }
        catch (NumberFormatException e)
        {
            return false;
        }
    }

    private static void createAndShowGUI()
    {
        JTextField textField = new JTextField(10);
        AbstractDocument doc = (AbstractDocument) textField.getDocument();
        doc.setDocumentFilter( new DoubleFilter() );
        textField.setText("123");
        textField.setText("123567");
        textField.setText(null);

        JFrame frame = new JFrame("Integer Filter");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLayout( new java.awt.GridBagLayout() );
        frame.add( textField );
        frame.setSize(220, 200);
        frame.setLocationByPlatform( true );
        frame.setVisible( true );
    }

    public static void main(String[] args) throws Exception
    {
        EventQueue.invokeLater( () -> createAndShowGUI() );
/*
        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                createAndShowGUI();
            }
        });
*/
    }

}

It doesn't do everything you want, but it should get you started.

camickr
  • 321,443
  • 19
  • 166
  • 288
  • it helped very much, Itook a lot of inperation from your answer, I’ll post what I did later today. it seems that method `insertString` doesn’t do anything, if it does, please let me know :-) – Roy Ash Dec 16 '20 at 07:51
  • 1
    @RoyAsh, *it seems that method insertString doesn’t do anything,* See: https://stackoverflow.com/questions/23525441/documentfilter-why-is-replace-invoked-and-not-insertstring/23525597#23525597. I always implement both methods (as demonstrated) to make sure I handle all situations. This allows for more reusable code. – camickr Dec 16 '20 at 15:33
  • 1
    My experience is that such filters will cause a bad user experience. Sometimes, it’s necessary to have an invalid text temporarily, to make editing simpler. An example is deleting the entire text to write an entirely new number. Doesn’t work, because an empty string is not a valid number. Or pasting a text that I know needs fixes afterwards. Not possible with that filter, so I need to open a second editor to make the fixes before pasting. Besides that, why re-invent [`JFormattedTextField`](https://docs.oracle.com/en/java/javase/15/docs/api/java.desktop/javax/swing/JFormattedTextField.html)? – Holger Dec 17 '20 at 09:36
  • @Holger, I’m still experimenting with the possibilities :-), besides, I remember that I’ve looked at it and I noticed that there is a formatter called something like `NumberFormatter` (I may have mistaken it’s name) that doen’t give me the wanted precision, I think it can be twickable but I went to Implementing simple `String` ↔︎ `double` covertion (sort of “you see what you get”), I may still take the `JFormattedTextField` option for the long run it may be the smarter move, I need to think about it. – Roy Ash Dec 17 '20 at 22:55
  • @Holger, I’ve tried it now, it appears that it doesn’t prevent the user from inserting characters that can’t continue to valid number (as the user type, not when the focus changes), is there a way to tweak its behavior to force its behavior per key-typed? – Roy Ash Dec 18 '20 at 13:48
  • @RoyAsh you can use a `DocumentFilter` as shown in this answer. But in case of a multi-character input, instead of rejecting the new text entirely, I’d remove the illegal characters from the input and still proceed with the remaining. Further, as said, I’d not insist on the result to be always a valid number. Only filter out the characters that can never contribute to a valid number. – Holger Dec 18 '20 at 13:57
0

My take on the solution, inspired by @camickr, and added 2 methods: getDouble() and setDouble()

import javax.swing.*;
import javax.swing.text.*;
import java.util.regex.Pattern;

public class JDoubleField extends JTextField
{
    private static final DocumentFilter doubleFilter = new DocumentFilter()
    {
        private final Pattern pattern = Pattern.compile("[+-]?(NaN|Infinity|\\d*(\\.\\d*)?((?<=\\d\\.?)[eE][+-]?\\d*)?)");

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

        @Override
        public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException
        {
            if (text == null || pattern.matcher(new StringBuilder(fb.getDocument().getText(0, fb.getDocument().getLength()))
                    .replace(offset, offset + length, text))
                    .matches())
                super.replace(fb, offset, length, text, attrs);
        }
    };

    public double getDouble()
    {
        try
        {
            return Double.parseDouble(getText());
        } catch (NumberFormatException ignored)
        {
            return Double.NaN;
        }
    }

    public void setDouble(double num)
    {
        setText(String.valueOf(num));
    }

    public JDoubleField()
    {
        this(0);
    }

    public JDoubleField(double num)
    {
        this(String.valueOf(num));
    }

    public JDoubleField(double num, int columns)
    {
        this(String.valueOf(num), columns);
    }

    public JDoubleField(Document doc, double num, int columns)
    {
        this(doc, String.valueOf(num), columns);
    }

    public JDoubleField(int columns)
    {
        this(null, columns);
    }

    public JDoubleField(String text)
    {
        this(text, 0);
    }

    public JDoubleField(String text, int columns)
    {
        this(null, text, columns);
    }

    public JDoubleField(Document doc, String text, int columns)
    {
        super(doc, null, columns);
        ((AbstractDocument) getDocument()).setDocumentFilter(doubleFilter);
        if (text != null)
            setText(text);
    }
}
Roy Ash
  • 1,078
  • 1
  • 11
  • 28
  • Don't extend JTextField. You only extend a class when you add functionality to the class. Setting the DocumentFilter is not adding functionality. – camickr Dec 16 '20 at 15:36
  • I wanted to make new type of text field, that has different behavior, and reusable, I didn’t add functionality, I’ve changed it. if I have lots of text-fields and I don’t want to change their behavior manually, I think it’s more preferable to extend JTextField – Roy Ash Dec 16 '20 at 15:40