001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.widgets;
003
004import java.awt.Color;
005import java.awt.event.ActionEvent;
006import java.awt.event.ActionListener;
007import java.awt.event.FocusEvent;
008import java.awt.event.FocusListener;
009import java.beans.PropertyChangeEvent;
010import java.beans.PropertyChangeListener;
011import java.util.Objects;
012
013import javax.swing.BorderFactory;
014import javax.swing.UIManager;
015import javax.swing.border.Border;
016import javax.swing.event.DocumentEvent;
017import javax.swing.event.DocumentListener;
018import javax.swing.text.JTextComponent;
019
020import org.openstreetmap.josm.tools.CheckParameterUtil;
021
022/**
023 * This is an abstract class for a validator on a text component.
024 *
025 * Subclasses implement {@link #validate()}. {@link #validate()} is invoked whenever
026 * <ul>
027 *   <li>the content of the text component changes (the validator is a {@link DocumentListener})</li>
028 *   <li>the text component loses focus (the validator is a {@link FocusListener})</li>
029 *   <li>the text component is a {@link JosmTextField} and an {@link ActionEvent} is detected</li>
030 * </ul>
031 *
032 *
033 */
034public abstract class AbstractTextComponentValidator implements ActionListener, FocusListener, DocumentListener, PropertyChangeListener {
035    private static final Border ERROR_BORDER = BorderFactory.createLineBorder(Color.RED, 1);
036    private static final Color ERROR_BACKGROUND = new Color(255, 224, 224);
037
038    private JTextComponent tc;
039    /** remembers whether the content of the text component is currently valid or not; null means,
040     * we don't know yet
041     */
042    private Boolean valid;
043    // remember the message
044    private String msg;
045
046    protected void feedbackInvalid(String msg) {
047        if (valid == null || valid || !Objects.equals(msg, this.msg)) {
048            // only provide feedback if the validity has changed. This avoids unnecessary UI updates.
049            tc.setBorder(ERROR_BORDER);
050            tc.setBackground(ERROR_BACKGROUND);
051            tc.setToolTipText(msg);
052            valid = Boolean.FALSE;
053            this.msg = msg;
054        }
055    }
056
057    protected void feedbackDisabled() {
058        feedbackValid(null);
059    }
060
061    protected void feedbackValid(String msg) {
062        if (valid == null || !valid || !Objects.equals(msg, this.msg)) {
063            // only provide feedback if the validity has changed. This avoids unnecessary UI updates.
064            tc.setBorder(UIManager.getBorder("TextField.border"));
065            tc.setBackground(UIManager.getColor("TextField.background"));
066            tc.setToolTipText(msg == null ? "" : msg);
067            valid = Boolean.TRUE;
068            this.msg = msg;
069        }
070    }
071
072    /**
073     * Replies the decorated text component
074     *
075     * @return the decorated text component
076     */
077    public JTextComponent getComponent() {
078        return tc;
079    }
080
081    /**
082     * Creates the validator and weires it to the text component <code>tc</code>.
083     *
084     * @param tc the text component. Must not be null.
085     * @throws IllegalArgumentException if tc is null
086     */
087    public AbstractTextComponentValidator(JTextComponent tc) {
088        this(tc, true);
089    }
090
091    /**
092     * Alternative constructor that allows to turn off the actionListener.
093     * This can be useful if the enter key stroke needs to be forwarded to the default button in a dialog.
094     * @param tc text component
095     * @param addActionListener {@code true} to add the action listener
096     */
097    public AbstractTextComponentValidator(JTextComponent tc, boolean addActionListener) {
098        this(tc, true, true, addActionListener);
099    }
100
101    /**
102     * Constructs a new {@code AbstractTextComponentValidator}.
103     * @param tc text component
104     * @param addFocusListener {@code true} to add the focus listener
105     * @param addDocumentListener {@code true} to add the document listener
106     * @param addActionListener {@code true} to add the action listener
107     */
108    public AbstractTextComponentValidator(JTextComponent tc, boolean addFocusListener, boolean addDocumentListener, boolean addActionListener) {
109        CheckParameterUtil.ensureParameterNotNull(tc, "tc");
110        this.tc = tc;
111        if (addFocusListener) {
112            tc.addFocusListener(this);
113        }
114        if (addDocumentListener) {
115            tc.getDocument().addDocumentListener(this);
116        }
117        if (addActionListener && tc instanceof JosmTextField) {
118            ((JosmTextField) tc).addActionListener(this);
119        }
120        tc.addPropertyChangeListener("enabled", this);
121    }
122
123    /**
124     * Implement in subclasses to validate the content of the text component.
125     *
126     */
127    public abstract void validate();
128
129    /**
130     * Replies true if the current content of the decorated text component is valid;
131     * false otherwise
132     *
133     * @return true if the current content of the decorated text component is valid
134     */
135    public abstract boolean isValid();
136
137    /* -------------------------------------------------------------------------------- */
138    /* interface FocusListener                                                          */
139    /* -------------------------------------------------------------------------------- */
140    @Override
141    public void focusGained(FocusEvent arg0) {}
142
143    @Override
144    public void focusLost(FocusEvent arg0) {
145        validate();
146    }
147
148    /* -------------------------------------------------------------------------------- */
149    /* interface ActionListener                                                         */
150    /* -------------------------------------------------------------------------------- */
151    @Override
152    public void actionPerformed(ActionEvent arg0) {
153        validate();
154    }
155
156    /* -------------------------------------------------------------------------------- */
157    /* interface DocumentListener                                                       */
158    /* -------------------------------------------------------------------------------- */
159    @Override
160    public void changedUpdate(DocumentEvent arg0) {
161        validate();
162    }
163
164    @Override
165    public void insertUpdate(DocumentEvent arg0) {
166        validate();
167    }
168
169    @Override
170    public void removeUpdate(DocumentEvent arg0) {
171        validate();
172    }
173
174    /* -------------------------------------------------------------------------------- */
175    /* interface PropertyChangeListener                                                 */
176    /* -------------------------------------------------------------------------------- */
177    @Override
178    public void propertyChange(PropertyChangeEvent evt) {
179        if ("enabled".equals(evt.getPropertyName())) {
180            boolean enabled = (Boolean) evt.getNewValue();
181            if (enabled) {
182                validate();
183            } else {
184                feedbackDisabled();
185            }
186        }
187    }
188}