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.data.tagging.ac.AutoCompletionItem; 023import org.openstreetmap.josm.gui.util.CellEditorSupport; 024import org.openstreetmap.josm.gui.widgets.JosmTextField; 025import org.openstreetmap.josm.spi.preferences.Config; 026import org.openstreetmap.josm.tools.Logging; 027 028/** 029 * AutoCompletingTextField is a text field with autocompletion behaviour. It 030 * can be used as table cell editor in {@link JTable}s. 031 * 032 * Autocompletion is controlled by a list of {@link AutoCompletionItem}s 033 * managed in a {@link AutoCompletionList}. 034 * 035 * @since 1762 036 */ 037public class AutoCompletingTextField extends JosmTextField implements ComboBoxEditor, TableCellEditor { 038 039 private Integer maxChars; 040 041 /** 042 * The document model for the editor 043 */ 044 class AutoCompletionDocument extends PlainDocument { 045 046 @Override 047 public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { 048 049 // If a maximum number of characters is specified, avoid exceeding it 050 if (maxChars != null && str != null && getLength() + str.length() > maxChars) { 051 int allowedLength = maxChars-getLength(); 052 if (allowedLength > 0) { 053 str = str.substring(0, allowedLength); 054 } else { 055 return; 056 } 057 } 058 059 if (autoCompletionList == null) { 060 super.insertString(offs, str, a); 061 return; 062 } 063 064 // input method for non-latin characters (e.g. scim) 065 if (a != null && a.isDefined(StyleConstants.ComposedTextAttribute)) { 066 super.insertString(offs, str, a); 067 return; 068 } 069 070 // if the current offset isn't at the end of the document we don't autocomplete. 071 // If a highlighted autocompleted suffix was present and we get here Swing has 072 // already removed it from the document. getLength() therefore doesn't include the 073 // autocompleted suffix. 074 // 075 if (offs < getLength()) { 076 super.insertString(offs, str, a); 077 return; 078 } 079 080 String currentText = getText(0, getLength()); 081 // if the text starts with a number we don't autocomplete 082 if (Config.getPref().getBoolean("autocomplete.dont_complete_numbers", true)) { 083 try { 084 Long.parseLong(str); 085 if (currentText.isEmpty()) { 086 // we don't autocomplete on numbers 087 super.insertString(offs, str, a); 088 return; 089 } 090 Long.parseLong(currentText); 091 super.insertString(offs, str, a); 092 return; 093 } catch (NumberFormatException e) { 094 // either the new text or the current text isn't a number. We continue with autocompletion 095 Logging.trace(e); 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.getFilteredItemAt(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 134 public void focusGained(FocusEvent e) { 135 if (e != null && e.getOppositeComponent() != null) { 136 // Select all characters when the change of focus occurs inside JOSM only. 137 // When switching from another application, it is annoying, see #13747 138 selectAll(); 139 } 140 applyFilter(getText()); 141 } 142 } 143 ); 144 145 addKeyListener( 146 new KeyAdapter() { 147 @Override 148 public void keyReleased(KeyEvent e) { 149 if (getText().isEmpty()) { 150 applyFilter(""); 151 } 152 } 153 } 154 ); 155 tableCellEditorSupport = new CellEditorSupport(this); 156 } 157 158 /** 159 * Constructs a new {@code AutoCompletingTextField}. 160 */ 161 public AutoCompletingTextField() { 162 this(0); 163 } 164 165 /** 166 * Constructs a new {@code AutoCompletingTextField}. 167 * @param columns the number of columns to use to calculate the preferred width; 168 * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation 169 */ 170 public AutoCompletingTextField(int columns) { 171 this(columns, true); 172 } 173 174 /** 175 * Constructs a new {@code AutoCompletingTextField}. 176 * @param columns the number of columns to use to calculate the preferred width; 177 * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation 178 * @param undoRedo Enables or not Undo/Redo feature. Not recommended for table cell editors, unless each cell provides its own editor 179 */ 180 public AutoCompletingTextField(int columns, boolean undoRedo) { 181 super(null, null, columns, undoRedo); 182 init(); 183 } 184 185 protected void applyFilter(String filter) { 186 if (autoCompletionList != null) { 187 autoCompletionList.applyFilter(filter); 188 } 189 } 190 191 /** 192 * Returns the auto completion list. 193 * @return the auto completion list; may be null, if no auto completion list is set 194 */ 195 public AutoCompletionList getAutoCompletionList() { 196 return autoCompletionList; 197 } 198 199 /** 200 * Sets the auto completion list. 201 * @param autoCompletionList the auto completion list; if null, auto completion is 202 * disabled 203 */ 204 public void setAutoCompletionList(AutoCompletionList autoCompletionList) { 205 this.autoCompletionList = autoCompletionList; 206 } 207 208 @Override 209 public Component getEditorComponent() { 210 return this; 211 } 212 213 @Override 214 public Object getItem() { 215 return getText(); 216 } 217 218 @Override 219 public void setItem(Object anObject) { 220 if (anObject == null) { 221 setText(""); 222 } else { 223 setText(anObject.toString()); 224 } 225 } 226 227 @Override 228 public void setText(String t) { 229 // disallow auto completion for this explicitly set string 230 this.noAutoCompletionString = t; 231 super.setText(t); 232 } 233 234 /** 235 * Sets the maximum number of characters allowed. 236 * @param max maximum number of characters allowed 237 * @since 5579 238 */ 239 public void setMaxChars(Integer max) { 240 maxChars = max; 241 } 242 243 /* ------------------------------------------------------------------------------------ */ 244 /* TableCellEditor interface */ 245 /* ------------------------------------------------------------------------------------ */ 246 247 private transient CellEditorSupport tableCellEditorSupport; 248 private String originalValue; 249 250 @Override 251 public void addCellEditorListener(CellEditorListener l) { 252 tableCellEditorSupport.addCellEditorListener(l); 253 } 254 255 protected void rememberOriginalValue(String value) { 256 this.originalValue = value; 257 } 258 259 protected void restoreOriginalValue() { 260 setText(originalValue); 261 } 262 263 @Override 264 public void removeCellEditorListener(CellEditorListener l) { 265 tableCellEditorSupport.removeCellEditorListener(l); 266 } 267 268 @Override 269 public void cancelCellEditing() { 270 restoreOriginalValue(); 271 tableCellEditorSupport.fireEditingCanceled(); 272 } 273 274 @Override 275 public Object getCellEditorValue() { 276 return getText(); 277 } 278 279 @Override 280 public boolean isCellEditable(EventObject anEvent) { 281 return true; 282 } 283 284 @Override 285 public boolean shouldSelectCell(EventObject anEvent) { 286 return true; 287 } 288 289 @Override 290 public boolean stopCellEditing() { 291 tableCellEditorSupport.fireEditingStopped(); 292 return true; 293 } 294 295 @Override 296 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 297 setText(value == null ? "" : value.toString()); 298 rememberOriginalValue(getText()); 299 return this; 300 } 301}