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 Main.trace(e); 094 } 095 } 096 String prefix = currentText.substring(0, offs); 097 autoCompletionList.applyFilter(prefix+str); 098 if (autoCompletionList.getFilteredSize() > 0 && !Objects.equals(str, noAutoCompletionString)) { 099 // there are matches. Insert the new text and highlight the auto completed suffix 100 String matchingString = autoCompletionList.getFilteredItem(0).getValue(); 101 remove(0, getLength()); 102 super.insertString(0, matchingString, a); 103 104 // highlight from insert position to end position to put the caret at the end 105 setCaretPosition(offs + str.length()); 106 moveCaretPosition(getLength()); 107 } else { 108 // there are no matches. Insert the new text, do not highlight 109 // 110 String newText = prefix + str; 111 remove(0, getLength()); 112 super.insertString(0, newText, a); 113 setCaretPosition(getLength()); 114 } 115 } 116 } 117 118 /** the auto completion list user input is matched against */ 119 protected AutoCompletionList autoCompletionList; 120 /** a string which should not be auto completed */ 121 protected String noAutoCompletionString; 122 123 @Override 124 protected Document createDefaultModel() { 125 return new AutoCompletionDocument(); 126 } 127 128 protected final void init() { 129 addFocusListener( 130 new FocusAdapter() { 131 @Override public void focusGained(FocusEvent e) { 132 selectAll(); 133 applyFilter(getText()); 134 } 135 } 136 ); 137 138 addKeyListener( 139 new KeyAdapter() { 140 141 @Override 142 public void keyReleased(KeyEvent e) { 143 if (getText().isEmpty()) { 144 applyFilter(""); 145 } 146 } 147 } 148 ); 149 tableCellEditorSupport = new CellEditorSupport(this); 150 } 151 152 /** 153 * Constructs a new {@code AutoCompletingTextField}. 154 */ 155 public AutoCompletingTextField() { 156 this(0); 157 } 158 159 /** 160 * Constructs a new {@code AutoCompletingTextField}. 161 * @param columns the number of columns to use to calculate the preferred width; 162 * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation 163 */ 164 public AutoCompletingTextField(int columns) { 165 this(columns, true); 166 } 167 168 /** 169 * Constructs a new {@code AutoCompletingTextField}. 170 * @param columns the number of columns to use to calculate the preferred width; 171 * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation 172 * @param undoRedo Enables or not Undo/Redo feature. Not recommended for table cell editors, unless each cell provides its own editor 173 */ 174 public AutoCompletingTextField(int columns, boolean undoRedo) { 175 super(null, null, columns, undoRedo); 176 init(); 177 } 178 179 protected void applyFilter(String filter) { 180 if (autoCompletionList != null) { 181 autoCompletionList.applyFilter(filter); 182 } 183 } 184 185 /** 186 * Returns the auto completion list. 187 * @return the auto completion list; may be null, if no auto completion list is set 188 */ 189 public AutoCompletionList getAutoCompletionList() { 190 return autoCompletionList; 191 } 192 193 /** 194 * Sets the auto completion list. 195 * @param autoCompletionList the auto completion list; if null, auto completion is 196 * disabled 197 */ 198 public void setAutoCompletionList(AutoCompletionList autoCompletionList) { 199 this.autoCompletionList = autoCompletionList; 200 } 201 202 @Override 203 public Component getEditorComponent() { 204 return this; 205 } 206 207 @Override 208 public Object getItem() { 209 return getText(); 210 } 211 212 @Override 213 public void setItem(Object anObject) { 214 if (anObject == null) { 215 setText(""); 216 } else { 217 setText(anObject.toString()); 218 } 219 } 220 221 @Override 222 public void setText(String t) { 223 // disallow auto completion for this explicitly set string 224 this.noAutoCompletionString = t; 225 super.setText(t); 226 } 227 228 /** 229 * Sets the maximum number of characters allowed. 230 * @param max maximum number of characters allowed 231 * @since 5579 232 */ 233 public void setMaxChars(Integer max) { 234 maxChars = max; 235 } 236 237 /* ------------------------------------------------------------------------------------ */ 238 /* TableCellEditor interface */ 239 /* ------------------------------------------------------------------------------------ */ 240 241 private transient CellEditorSupport tableCellEditorSupport; 242 private String originalValue; 243 244 @Override 245 public void addCellEditorListener(CellEditorListener l) { 246 tableCellEditorSupport.addCellEditorListener(l); 247 } 248 249 protected void rememberOriginalValue(String value) { 250 this.originalValue = value; 251 } 252 253 protected void restoreOriginalValue() { 254 setText(originalValue); 255 } 256 257 @Override 258 public void removeCellEditorListener(CellEditorListener l) { 259 tableCellEditorSupport.removeCellEditorListener(l); 260 } 261 262 @Override 263 public void cancelCellEditing() { 264 restoreOriginalValue(); 265 tableCellEditorSupport.fireEditingCanceled(); 266 } 267 268 @Override 269 public Object getCellEditorValue() { 270 return getText(); 271 } 272 273 @Override 274 public boolean isCellEditable(EventObject anEvent) { 275 return true; 276 } 277 278 @Override 279 public boolean shouldSelectCell(EventObject anEvent) { 280 return true; 281 } 282 283 @Override 284 public boolean stopCellEditing() { 285 tableCellEditorSupport.fireEditingStopped(); 286 return true; 287 } 288 289 @Override 290 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 291 setText(value == null ? "" : value.toString()); 292 rememberOriginalValue(getText()); 293 return this; 294 } 295}