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}