001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.tagging.ac; 003 004import java.awt.Component; 005import java.awt.event.FocusAdapter; 006import java.awt.event.FocusEvent; 007import java.awt.event.KeyAdapter; 008import java.awt.event.KeyEvent; 009import java.util.EventObject; 010import java.util.Objects; 011 012import javax.swing.ComboBoxEditor; 013import javax.swing.JTable; 014import javax.swing.event.CellEditorListener; 015import javax.swing.table.TableCellEditor; 016import javax.swing.text.AttributeSet; 017import javax.swing.text.BadLocationException; 018import javax.swing.text.Document; 019import javax.swing.text.PlainDocument; 020import javax.swing.text.StyleConstants; 021 022import org.openstreetmap.josm.Main; 023import org.openstreetmap.josm.gui.util.CellEditorSupport; 024import org.openstreetmap.josm.gui.widgets.JosmTextField; 025 026/** 027 * AutoCompletingTextField is a text field with autocompletion behaviour. It 028 * can be used as table cell editor in {@link JTable}s. 029 * 030 * Autocompletion is controlled by a list of {@link AutoCompletionListItem}s 031 * managed in a {@link AutoCompletionList}. 032 * 033 * @since 1762 034 */ 035public class AutoCompletingTextField extends JosmTextField implements ComboBoxEditor, TableCellEditor { 036 037 private Integer maxChars; 038 039 /** 040 * The document model for the editor 041 */ 042 class AutoCompletionDocument extends PlainDocument { 043 044 @Override 045 public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { 046 047 // If a maximum number of characters is specified, avoid to exceed it 048 if (maxChars != null && str != null && getLength() + str.length() > maxChars) { 049 int allowedLength = maxChars-getLength(); 050 if (allowedLength > 0) { 051 str = str.substring(0, allowedLength); 052 } else { 053 return; 054 } 055 } 056 057 if (autoCompletionList == null) { 058 super.insertString(offs, str, a); 059 return; 060 } 061 062 // input method for non-latin characters (e.g. scim) 063 if (a != null && a.isDefined(StyleConstants.ComposedTextAttribute)) { 064 super.insertString(offs, str, a); 065 return; 066 } 067 068 // if the current offset isn't at the end of the document we don't autocomplete. 069 // If a highlighted autocompleted suffix was present and we get here Swing has 070 // already removed it from the document. getLength() therefore doesn't include the 071 // autocompleted suffix. 072 // 073 if (offs < getLength()) { 074 super.insertString(offs, str, a); 075 return; 076 } 077 078 String currentText = getText(0, getLength()); 079 // if the text starts with a number we don't autocomplete 080 if (Main.pref.getBoolean("autocomplete.dont_complete_numbers", true)) { 081 try { 082 Long.parseLong(str); 083 if (currentText.isEmpty()) { 084 // we don't autocomplete on numbers 085 super.insertString(offs, str, a); 086 return; 087 } 088 Long.parseLong(currentText); 089 super.insertString(offs, str, a); 090 return; 091 } catch (NumberFormatException e) { 092 // either the new text or the current text isn't a number. We continue with autocompletion 093 if (Main.isTraceEnabled()) { 094 Main.trace(e.getMessage()); 095 } 096 } 097 } 098 String prefix = currentText.substring(0, offs); 099 autoCompletionList.applyFilter(prefix+str); 100 if (autoCompletionList.getFilteredSize() > 0 && !Objects.equals(str, noAutoCompletionString)) { 101 // there are matches. Insert the new text and highlight the auto completed suffix 102 String matchingString = autoCompletionList.getFilteredItem(0).getValue(); 103 remove(0, getLength()); 104 super.insertString(0, matchingString, a); 105 106 // highlight from insert position to end position to put the caret at the end 107 setCaretPosition(offs + str.length()); 108 moveCaretPosition(getLength()); 109 } else { 110 // there are no matches. Insert the new text, do not highlight 111 // 112 String newText = prefix + str; 113 remove(0, getLength()); 114 super.insertString(0, newText, a); 115 setCaretPosition(getLength()); 116 } 117 } 118 } 119 120 /** the auto completion list user input is matched against */ 121 protected AutoCompletionList autoCompletionList; 122 /** a string which should not be auto completed */ 123 protected String noAutoCompletionString; 124 125 @Override 126 protected Document createDefaultModel() { 127 return new AutoCompletionDocument(); 128 } 129 130 protected final void init() { 131 addFocusListener( 132 new FocusAdapter() { 133 @Override public void focusGained(FocusEvent e) { 134 selectAll(); 135 applyFilter(getText()); 136 } 137 } 138 ); 139 140 addKeyListener( 141 new KeyAdapter() { 142 143 @Override 144 public void keyReleased(KeyEvent e) { 145 if (getText().isEmpty()) { 146 applyFilter(""); 147 } 148 } 149 } 150 ); 151 tableCellEditorSupport = new CellEditorSupport(this); 152 } 153 154 /** 155 * Constructs a new {@code AutoCompletingTextField}. 156 */ 157 public AutoCompletingTextField() { 158 this(0); 159 } 160 161 /** 162 * Constructs a new {@code AutoCompletingTextField}. 163 * @param columns the number of columns to use to calculate the preferred width; 164 * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation 165 */ 166 public AutoCompletingTextField(int columns) { 167 this(columns, true); 168 } 169 170 /** 171 * Constructs a new {@code AutoCompletingTextField}. 172 * @param columns the number of columns to use to calculate the preferred width; 173 * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation 174 * @param undoRedo Enables or not Undo/Redo feature. Not recommended for table cell editors, unless each cell provides its own editor 175 */ 176 public AutoCompletingTextField(int columns, boolean undoRedo) { 177 super(null, null, columns, undoRedo); 178 init(); 179 } 180 181 protected void applyFilter(String filter) { 182 if (autoCompletionList != null) { 183 autoCompletionList.applyFilter(filter); 184 } 185 } 186 187 /** 188 * Returns the auto completion list. 189 * @return the auto completion list; may be null, if no auto completion list is set 190 */ 191 public AutoCompletionList getAutoCompletionList() { 192 return autoCompletionList; 193 } 194 195 /** 196 * Sets the auto completion list. 197 * @param autoCompletionList the auto completion list; if null, auto completion is 198 * disabled 199 */ 200 public void setAutoCompletionList(AutoCompletionList autoCompletionList) { 201 this.autoCompletionList = autoCompletionList; 202 } 203 204 @Override 205 public Component getEditorComponent() { 206 return this; 207 } 208 209 @Override 210 public Object getItem() { 211 return getText(); 212 } 213 214 @Override 215 public void setItem(Object anObject) { 216 if (anObject == null) { 217 setText(""); 218 } else { 219 setText(anObject.toString()); 220 } 221 } 222 223 @Override 224 public void setText(String t) { 225 // disallow auto completion for this explicitly set string 226 this.noAutoCompletionString = t; 227 super.setText(t); 228 } 229 230 /** 231 * Sets the maximum number of characters allowed. 232 * @param max maximum number of characters allowed 233 * @since 5579 234 */ 235 public void setMaxChars(Integer max) { 236 maxChars = max; 237 } 238 239 /* ------------------------------------------------------------------------------------ */ 240 /* TableCellEditor interface */ 241 /* ------------------------------------------------------------------------------------ */ 242 243 private transient CellEditorSupport tableCellEditorSupport; 244 private String originalValue; 245 246 @Override 247 public void addCellEditorListener(CellEditorListener l) { 248 tableCellEditorSupport.addCellEditorListener(l); 249 } 250 251 protected void rememberOriginalValue(String value) { 252 this.originalValue = value; 253 } 254 255 protected void restoreOriginalValue() { 256 setText(originalValue); 257 } 258 259 @Override 260 public void removeCellEditorListener(CellEditorListener l) { 261 tableCellEditorSupport.removeCellEditorListener(l); 262 } 263 264 @Override 265 public void cancelCellEditing() { 266 restoreOriginalValue(); 267 tableCellEditorSupport.fireEditingCanceled(); 268 } 269 270 @Override 271 public Object getCellEditorValue() { 272 return getText(); 273 } 274 275 @Override 276 public boolean isCellEditable(EventObject anEvent) { 277 return true; 278 } 279 280 @Override 281 public boolean shouldSelectCell(EventObject anEvent) { 282 return true; 283 } 284 285 @Override 286 public boolean stopCellEditing() { 287 tableCellEditorSupport.fireEditingStopped(); 288 return true; 289 } 290 291 @Override 292 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 293 setText(value == null ? "" : value.toString()); 294 rememberOriginalValue(getText()); 295 return this; 296 } 297}