0

I want to pait a red border on textfields in which the user enters an invalid value. This fields can have a invalid value even if the user hasn´t edited them, for example a null value on a DateFormatter. Or a null value on a NumberFormatter. That is why I'm using a FocusListener.

Here are two classes that do what I need, however, when the application sets or changes the value without user input, then the field keeps appearing as invalid.

I am not sure if I should use "look and feel" as a solution, or maybe "actions". Do you see a good OOP approach on the way I am trying to implement this need?

Thanks in advance.

public class Form {
    private java.util.ArrayList<JComponent> list = new java.util.ArrayList();
    private TextVerifier verifier;

    public Form() {
        verifier = new TextVerifier();
    }
    public void add(JComponent c) {
        if (c instanceof JFormattedTextField) {
            ((JFormattedTextField)c).setFocusLostBehavior(JFormattedTextField.COMMIT);
            c.addFocusListener(verifier);
        }
    }
    public void clear() {
        for (JComponent c : list) {
            verifier.unmark(c);
        }
    }
}
public class TextVerifier implements FocusListener {
    @Override
    public void focusGained(FocusEvent e) {

    }
    @Override
    public void focusLost(FocusEvent e) {
        if (e.getSource() instanceof JFormattedTextField) {
            JFormattedTextField field = (JFormattedTextField)e.getSource();
            if (isValid(field)) {
                unmark(field);
            }
            else {
                mark(field);
            }
        }
    }
    public void mark(JComponent component) {
        if (component!=null) {
            component.setBorder(javax.swing.BorderFactory.createLineBorder(java.awt.Color.red, 1));
        }            
    }
    public void unmark(JComponent component) {
        if (component!=null) {
            component.setBorder(null);
        }
    }

    public boolean isValid(JFormattedTextField field) {
        JFormattedTextField.AbstractFormatter formatter = field.getFormatter();
        if (formatter != null) {
            try {
                formatter.stringToValue(field.getText());
            }
            catch (java.text.ParseException e) {
                return false;
            }
        }
        return true;
    }
}
AlbertoLopez
  • 101
  • 3
  • You may find that changing the components border modifies the UI in away which is unpleasant - text fields have their own borders – MadProgrammer Jan 16 '18 at 02:13
  • I spent some time awhile ago investigating different ways to do this, [this solution is based on using the `JLayer`/`JXLayer` API](https://stackoverflow.com/questions/25274566/how-can-i-change-the-highlight-color-of-a-focused-jcombobox/25276658#25276658) which basically does the same thing. The important part here is 1- It doesn't modify the underlying component and 2- It won't have a visual effect on the layout – MadProgrammer Jan 16 '18 at 02:15
  • MadProgrammer. Thanks for the link you provide. It references some concepts/classes I haven´t learned yet (UI's etc). I will have the link in hand for when I learn a few more concepts. At the moment, I´m seeking a simpler solution in order to meet dead-lines. I appreciate your suggestions. thanks – AlbertoLopez Jan 16 '18 at 03:19

1 Answers1

0

I would prefer using a DocumentListener. To address your problem I think this would be the best way. Note the mark and unmark methods in TextVerifier, to avoid issues with the lookAndFeel, it's probably better to just change the background of the text field, which looks way better than a red color border on the text field.

    public void add(JComponent c) {
        if (c instanceof JFormattedTextField) {
            ((JFormattedTextField) c).setFocusLostBehavior(JFormattedTextField.COMMIT);
            c.addFocusListener(verifier);
                            //Add the component to the document properties
            ((JFormattedTextField) c).getDocument().putProperty("owner", c);
                            //Change Verifier to implement DocumentListener. You wouldn't need focusListener in this case.
            ((JFormattedTextField) c).getDocument().addDocumentListener(verifier);
            list.add(c);
        }
    }

Complete Code below:

import java.awt.BorderLayout;
import java.util.Random;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.text.NumberFormatter;

public class Form {
    private java.util.ArrayList<JComponent> list = new java.util.ArrayList<>();
    private TextVerifier verifier;

    public Form() {
        verifier = new TextVerifier();
    }

    public void add(JComponent c) {
        if (c instanceof JFormattedTextField) {
            ((JFormattedTextField) c).setFocusLostBehavior(JFormattedTextField.COMMIT);
            c.addFocusListener(verifier);
            ((JFormattedTextField) c).getDocument().putProperty("owner", c);
            ((JFormattedTextField) c).getDocument().addDocumentListener(verifier);
            list.add(c);
        }
    }

    public void clear() {
        for (JComponent c : list) {
            verifier.unmark(c);
        }
    }

    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        frame.getContentPane().add(panel, BorderLayout.CENTER);
        Form form = new Form();
        for (int i = 0; i < 5; i++) {
            JLabel label = new JLabel("Number " + i + ": ");
            JFormattedTextField field = new JFormattedTextField(new NumberFormatter());
            panel.add(createLabelFieldPanel(label, field));
            form.add(field);
        }

        JButton change = new JButton("Change");
        change.addActionListener(e -> form.updateValues());

        frame.getContentPane().add(change, BorderLayout.SOUTH);
        frame.pack();
        form.updateValues();
        frame.setVisible(true);
    }

    private Random random = new Random();

    private void updateValues() {
        for (JComponent c : list) {
            if (c instanceof JFormattedTextField) {
                ((JFormattedTextField) c).setValue(random.nextInt(100));
            }
        }
    }

    public static JPanel createLabelFieldPanel(final JComponent label, final JComponent field) {
        final JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
        panel.add(label);
        panel.add(field);
        return panel;
    }
}

import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;

import javax.swing.JComponent;
import javax.swing.JFormattedTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

public class TextVerifier implements FocusListener, DocumentListener {
    @Override
    public void focusGained(FocusEvent e) {

    }

    @Override
    public void focusLost(FocusEvent e) {
        if (e.getSource() instanceof JFormattedTextField) {
            JFormattedTextField field = (JFormattedTextField) e.getSource();
            if (isValid(field)) {
                unmark(field);
            } else {
                mark(field);
            }
        }
    }

private static final Color RED = hex2Rgb("fffeeeddd");

public static Color hex2Rgb(String colorStr) {
    return new Color(
            Integer.valueOf( colorStr.substring( 1, 3 ), 16 ),
            Integer.valueOf( colorStr.substring( 3, 5 ), 16 ),
            Integer.valueOf( colorStr.substring( 5, 7 ), 16 ) );
}

public void mark(JComponent component) {
    if (component != null) {
        component.setBackground(RED);
    }
}

public void unmark(JComponent component) {
    if (component != null) {
        component.setBackground(Color.white);
    }
}

    public boolean isValid(JFormattedTextField field) {
        JFormattedTextField.AbstractFormatter formatter = field.getFormatter();
        if (formatter != null) {
            try {
                formatter.stringToValue(field.getText());
            } catch (java.text.ParseException e) {
                return false;
            }
        }
        return true;
    }

    @Override
    public void changedUpdate(DocumentEvent e) {
        docChanged(e);
    }

    @Override
    public void insertUpdate(DocumentEvent e) {
        docChanged(e);
    }

    @Override
    public void removeUpdate(DocumentEvent e) {
        docChanged(e);
    }

    private void docChanged(DocumentEvent e) {
        if (e.getDocument().getProperty("owner") instanceof JFormattedTextField) {
            JFormattedTextField field = (JFormattedTextField) e.getDocument().getProperty("owner");
            if (isValid(field)) {
                unmark(field);
            } else {
                mark(field);
            }
        }
    }
}
  • Nitpick - what happens if the fields default background color isn't white? – MadProgrammer Jan 16 '18 at 02:54
  • I think the whole point is to notify the user that there is something wrong with the input, if the color is not white, just use some other color that does not match the default color of the component. Just a matter of training the user on what to expect when something wrong is entered, this question could be applied to whatever solution we come up with to identify errors. What happens when the border default color is red. so on and so forth. I see your point though. – Aravind Chennuru Jan 16 '18 at 03:18
  • @Aravind. Your suggestion is very interesting. I will try it out. Thank you. – AlbertoLopez Jan 16 '18 at 03:21
  • Regarding the text background, I could put the user's default in the clientProperty map of JFormattedTextField when I add it to the Form. But at the moment all fields are white. thanks. – AlbertoLopez Jan 16 '18 at 03:22
  • Aravind, I get the same result adding a "validEdit" propertychangelistener to each JFormattedTextfield with lot less trouble. I making the test with this alternative. – AlbertoLopez Jan 17 '18 at 03:44