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