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}