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     */
095    public AbstractTextComponentValidator(JTextComponent tc, boolean addActionListener) {
096        this(tc, true, true, addActionListener);
097    }
098
099    public AbstractTextComponentValidator(JTextComponent tc, boolean addFocusListener, boolean addDocumentListener, boolean addActionListener) {
100        CheckParameterUtil.ensureParameterNotNull(tc, "tc");
101        this.tc = tc;
102        if (addFocusListener) {
103            tc.addFocusListener(this);
104        }
105        if (addDocumentListener) {
106            tc.getDocument().addDocumentListener(this);
107        }
108        if (addActionListener) {
109            if (tc instanceof JosmTextField) {
110                JosmTextField tf = (JosmTextField) tc;
111                tf.addActionListener(this);
112            }
113        }
114        tc.addPropertyChangeListener("enabled", this);
115    }
116
117    /**
118     * Implement in subclasses to validate the content of the text component.
119     *
120     */
121    public abstract void validate();
122
123    /**
124     * Replies true if the current content of the decorated text component is valid;
125     * false otherwise
126     *
127     * @return true if the current content of the decorated text component is valid
128     */
129    public abstract boolean isValid();
130
131    /* -------------------------------------------------------------------------------- */
132    /* interface FocusListener                                                          */
133    /* -------------------------------------------------------------------------------- */
134    @Override
135    public void focusGained(FocusEvent arg0) {}
136
137    @Override
138    public void focusLost(FocusEvent arg0) {
139        validate();
140    }
141
142    /* -------------------------------------------------------------------------------- */
143    /* interface ActionListener                                                         */
144    /* -------------------------------------------------------------------------------- */
145    @Override
146    public void actionPerformed(ActionEvent arg0) {
147        validate();
148    }
149
150    /* -------------------------------------------------------------------------------- */
151    /* interface DocumentListener                                                       */
152    /* -------------------------------------------------------------------------------- */
153    @Override
154    public void changedUpdate(DocumentEvent arg0) {
155        validate();
156    }
157
158    @Override
159    public void insertUpdate(DocumentEvent arg0) {
160        validate();
161    }
162
163    @Override
164    public void removeUpdate(DocumentEvent arg0) {
165        validate();
166    }
167
168    /* -------------------------------------------------------------------------------- */
169    /* interface PropertyChangeListener                                                 */
170    /* -------------------------------------------------------------------------------- */
171    @Override
172    public void propertyChange(PropertyChangeEvent evt) {
173        if ("enabled".equals(evt.getPropertyName())) {
174            boolean enabled = (Boolean) evt.getNewValue();
175            if (enabled) {
176                validate();
177            } else {
178                feedbackDisabled();
179            }
180        }
181    }
182}