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) {
118            if (tc instanceof JosmTextField) {
119                JosmTextField tf = (JosmTextField) tc;
120                tf.addActionListener(this);
121            }
122        }
123        tc.addPropertyChangeListener("enabled", this);
124    }
125
126    /**
127     * Implement in subclasses to validate the content of the text component.
128     *
129     */
130    public abstract void validate();
131
132    /**
133     * Replies true if the current content of the decorated text component is valid;
134     * false otherwise
135     *
136     * @return true if the current content of the decorated text component is valid
137     */
138    public abstract boolean isValid();
139
140    /* -------------------------------------------------------------------------------- */
141    /* interface FocusListener                                                          */
142    /* -------------------------------------------------------------------------------- */
143    @Override
144    public void focusGained(FocusEvent arg0) {}
145
146    @Override
147    public void focusLost(FocusEvent arg0) {
148        validate();
149    }
150
151    /* -------------------------------------------------------------------------------- */
152    /* interface ActionListener                                                         */
153    /* -------------------------------------------------------------------------------- */
154    @Override
155    public void actionPerformed(ActionEvent arg0) {
156        validate();
157    }
158
159    /* -------------------------------------------------------------------------------- */
160    /* interface DocumentListener                                                       */
161    /* -------------------------------------------------------------------------------- */
162    @Override
163    public void changedUpdate(DocumentEvent arg0) {
164        validate();
165    }
166
167    @Override
168    public void insertUpdate(DocumentEvent arg0) {
169        validate();
170    }
171
172    @Override
173    public void removeUpdate(DocumentEvent arg0) {
174        validate();
175    }
176
177    /* -------------------------------------------------------------------------------- */
178    /* interface PropertyChangeListener                                                 */
179    /* -------------------------------------------------------------------------------- */
180    @Override
181    public void propertyChange(PropertyChangeEvent evt) {
182        if ("enabled".equals(evt.getPropertyName())) {
183            boolean enabled = (Boolean) evt.getNewValue();
184            if (enabled) {
185                validate();
186            } else {
187                feedbackDisabled();
188            }
189        }
190    }
191}