001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.tagging.ac; 003 004import java.awt.Component; 005import java.awt.datatransfer.Clipboard; 006import java.awt.datatransfer.StringSelection; 007import java.awt.datatransfer.Transferable; 008import java.awt.event.KeyEvent; 009import java.awt.event.KeyListener; 010import java.util.EventObject; 011import java.util.regex.Pattern; 012 013import javax.swing.JTable; 014import javax.swing.SwingUtilities; 015import javax.swing.event.CellEditorListener; 016import javax.swing.table.TableCellEditor; 017import javax.swing.text.AbstractDocument; 018 019import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils; 020import org.openstreetmap.josm.gui.util.CellEditorSupport; 021import org.openstreetmap.josm.gui.widgets.JosmTextField; 022import org.openstreetmap.josm.spi.preferences.Config; 023 024/** 025 * An auto-completing TextField. 026 * <p> 027 * When the user starts typing, this textfield will suggest the 028 * {@link AutoCompComboBoxModel#findBestCandidate best matching item} from its model. The items in 029 * the model can be of any type while the items' {@code toString} values are used for 030 * autocompletion. 031 * 032 * @param <E> the type of items in the model 033 * @since 18221 034 */ 035public class AutoCompTextField<E> extends JosmTextField implements TableCellEditor, KeyListener { 036 037 /** true if the combobox should autocomplete */ 038 private boolean autocompleteEnabled = true; 039 /** a filter to enforce max. text length */ 040 private transient MaxLengthDocumentFilter docFilter; 041 /** the model */ 042 protected AutoCompComboBoxModel<E> model; 043 /** Whether to autocomplete numbers */ 044 private final boolean AUTOCOMPLETE_NUMBERS = !Config.getPref().getBoolean("autocomplete.dont_complete_numbers", true); 045 /** a regex that matches numbers */ 046 private static final Pattern IS_NUMBER = Pattern.compile("^\\d+$"); 047 048 protected final void init() { 049 model = new AutoCompComboBoxModel<>(); 050 docFilter = new MaxLengthDocumentFilter(); 051 ((AbstractDocument) getDocument()).setDocumentFilter(docFilter); 052 addKeyListener(this); 053 tableCellEditorSupport = new CellEditorSupport(this); 054 } 055 056 /** 057 * Constructs a new {@code AutoCompTextField}. 058 */ 059 public AutoCompTextField() { 060 this(0); 061 } 062 063 /** 064 * Constructs a new {@code AutoCompTextField}. 065 * @param model the model to use 066 */ 067 public AutoCompTextField(AutoCompComboBoxModel<E> model) { 068 this(0); 069 this.model = model; 070 } 071 072 /** 073 * Constructs a new {@code AutoCompTextField}. 074 * @param columns the number of columns to use to calculate the preferred width; 075 * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation 076 */ 077 public AutoCompTextField(int columns) { 078 this(columns, true); 079 } 080 081 /** 082 * Constructs a new {@code AutoCompTextField}. 083 * @param columns the number of columns to use to calculate the preferred width; 084 * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation 085 * @param undoRedo Enables or not Undo/Redo feature. Not recommended for table cell editors, unless each cell provides its own editor 086 */ 087 public AutoCompTextField(int columns, boolean undoRedo) { 088 super(null, null, columns, undoRedo); 089 init(); 090 } 091 092 /** 093 * Returns the {@link AutoCompComboBoxModel} currently used. 094 * 095 * @return the model 096 */ 097 public AutoCompComboBoxModel<E> getModel() { 098 return model; 099 } 100 101 /** 102 * Sets the data model that the {@code AutoCompTextField} uses to obtain the list of items. 103 * 104 * @param model the {@link AutoCompComboBoxModel} that provides the list of items used for autocomplete 105 */ 106 public void setModel(AutoCompComboBoxModel<E> model) { 107 AutoCompComboBoxModel<E> oldModel = this.model; 108 this.model = model; 109 firePropertyChange("model", oldModel, model); 110 } 111 112 /** 113 * Returns {@code true} if autocompletion is enabled. 114 * 115 * @return {@code true} if autocompletion is enabled. 116 */ 117 public final boolean isAutocompleteEnabled() { 118 return autocompleteEnabled; 119 } 120 121 /** 122 * Enables or disables the autocompletion. 123 * 124 * @param enabled {@code true} to enable autocompletion 125 * @return {@code true} if autocomplete was enabled before calling this 126 */ 127 public boolean setAutocompleteEnabled(boolean enabled) { 128 boolean oldEnabled = this.autocompleteEnabled; 129 this.autocompleteEnabled = enabled; 130 return oldEnabled; 131 } 132 133 /** 134 * Sets the maximum number of characters allowed. 135 * @param length maximum number of characters allowed 136 */ 137 public void setMaxTextLength(int length) { 138 docFilter.setMaxLength(length); 139 } 140 141 /** 142 * Autocompletes what the user typed in. 143 * <p> 144 * Gets the user input from the editor, finds the best matching item in the model, sets the 145 * editor text to it, and highlights the autocompleted part. If there is no matching item, removes the 146 * list selection. 147 * 148 * @param oldText the text before the last keypress was processed 149 */ 150 private void autocomplete(String oldText) { 151 String newText = getText(); 152 if (getSelectionEnd() != newText.length()) 153 // selection not at the end 154 return; 155 // if the user typed some control character (eg. Alt+A) the selection may still be there 156 String unSelected = newText.substring(0, getSelectionStart()); 157 if (unSelected.length() <= oldText.length()) 158 // do not autocomplete on control or deleted chars 159 return; 160 if (!AUTOCOMPLETE_NUMBERS && IS_NUMBER.matcher(newText).matches()) 161 return; 162 163 fireAutoCompEvent(AutoCompEvent.AUTOCOMP_BEFORE, null); 164 E item = getModel().findBestCandidate(newText); 165 fireAutoCompEvent(AutoCompEvent.AUTOCOMP_DONE, item); 166 167 if (item != null) { 168 String text = item.toString(); 169 setText(text); 170 // select the autocompleted suffix in the editor 171 select(newText.length(), text.length()); 172 // copy the whole autocompleted string to the unix system-wide selection (aka 173 // middle-click), else only the selected suffix would be copied 174 copyToSysSel(text); 175 } 176 } 177 178 /** 179 * Copies a String to the UNIX system-wide selection (aka middle-click). 180 * 181 * @param s the string to copy 182 */ 183 void copyToSysSel(String s) { 184 Clipboard sysSel = ClipboardUtils.getSystemSelection(); 185 if (sysSel != null) { 186 Transferable transferable = new StringSelection(s); 187 sysSel.setContents(transferable, null); 188 } 189 } 190 191 /** 192 * Adds an AutoCompListener. 193 * 194 * @param l the AutoComp listener to be added 195 */ 196 public synchronized void addAutoCompListener(AutoCompListener l) { 197 listenerList.add(AutoCompListener.class, l); 198 } 199 200 /** 201 * Removes the specified AutoCompListener. 202 * 203 * @param l the autoComp listener to be removed 204 */ 205 public synchronized void removeAutoCompListener(AutoCompListener l) { 206 listenerList.remove(AutoCompListener.class, l); 207 } 208 209 /** 210 * Returns an array of all the current <code>AutoCompListener</code>s. 211 * 212 * @return all of the <code>AutoCompListener</code>s added or an empty 213 * array if no listeners have been added 214 */ 215 public synchronized AutoCompListener[] getAutoCompListeners() { 216 return listenerList.getListeners(AutoCompListener.class); 217 } 218 219 /** 220 * Notifies all listeners that have registered interest for notification on this event type. 221 * The event instance is lazily created. The listener list is processed in last to first order. 222 * 223 * @param id The Autocomp event id 224 * @param item The item selected for autocompletion. 225 * @see javax.swing.event.EventListenerList 226 */ 227 protected void fireAutoCompEvent(int id, Object item) { 228 // Guaranteed to return a non-null array 229 Object[] listeners = listenerList.getListenerList(); 230 AutoCompEvent e = new AutoCompEvent(this, id, item); 231 232 // Process the listeners last to first, notifying 233 // those that are interested in this event 234 for (int i = listeners.length - 2; i >= 0; i -= 2) { 235 if (listeners[i] == AutoCompListener.class) { 236 switch (id) { 237 case AutoCompEvent.AUTOCOMP_DONE: 238 ((AutoCompListener) listeners[i + 1]).autoCompPerformed(e); 239 break; 240 case AutoCompEvent.AUTOCOMP_BEFORE: 241 ((AutoCompListener) listeners[i + 1]).autoCompBefore(e); 242 break; 243 default: 244 break; 245 } 246 } 247 } 248 } 249 250 /* ------------------------------------------------------------------------------------ */ 251 /* KeyListener interface */ 252 /* ------------------------------------------------------------------------------------ */ 253 254 /** 255 * Listens to key events and eventually schedules an autocomplete. 256 * 257 * @param e the key event 258 */ 259 @Override 260 public void keyTyped(KeyEvent e) { 261 // if selection is at the end 262 if (autocompleteEnabled && getSelectionEnd() == getText().length()) { 263 final String oldText = getText().substring(0, getSelectionStart()); 264 // We got the event before the editor component could see it. Let the editor do its job first. 265 SwingUtilities.invokeLater(() -> autocomplete(oldText)); 266 } 267 } 268 269 @Override 270 public void keyPressed(KeyEvent e) { 271 // not interested 272 } 273 274 @Override 275 public void keyReleased(KeyEvent e) { 276 // not interested 277 } 278 279 /* ------------------------------------------------------------------------------------ */ 280 /* TableCellEditor interface */ 281 /* ------------------------------------------------------------------------------------ */ 282 283 private transient CellEditorSupport tableCellEditorSupport; 284 private String originalValue; 285 286 @Override 287 public void addCellEditorListener(CellEditorListener l) { 288 tableCellEditorSupport.addCellEditorListener(l); 289 } 290 291 protected void rememberOriginalValue(String value) { 292 this.originalValue = value; 293 } 294 295 protected void restoreOriginalValue() { 296 setText(originalValue); 297 } 298 299 @Override 300 public void removeCellEditorListener(CellEditorListener l) { 301 tableCellEditorSupport.removeCellEditorListener(l); 302 } 303 304 @Override 305 public void cancelCellEditing() { 306 restoreOriginalValue(); 307 tableCellEditorSupport.fireEditingCanceled(); 308 } 309 310 @Override 311 public Object getCellEditorValue() { 312 return getText(); 313 } 314 315 @Override 316 public boolean isCellEditable(EventObject anEvent) { 317 return true; 318 } 319 320 @Override 321 public boolean shouldSelectCell(EventObject anEvent) { 322 return true; 323 } 324 325 @Override 326 public boolean stopCellEditing() { 327 tableCellEditorSupport.fireEditingStopped(); 328 return true; 329 } 330 331 @Override 332 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 333 setText(value == null ? "" : value.toString()); 334 rememberOriginalValue(getText()); 335 return this; 336 } 337}