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; 010 011import javax.swing.ComboBoxEditor; 012import javax.swing.JTable; 013import javax.swing.event.CellEditorListener; 014import javax.swing.table.TableCellEditor; 015import javax.swing.text.AttributeSet; 016import javax.swing.text.BadLocationException; 017import javax.swing.text.Document; 018import javax.swing.text.PlainDocument; 019import javax.swing.text.StyleConstants; 020 021import org.openstreetmap.josm.Main; 022import org.openstreetmap.josm.gui.util.CellEditorSupport; 023import org.openstreetmap.josm.gui.widgets.JosmTextField; 024 025/** 026 * AutoCompletingTextField is a text field with autocompletion behaviour. It 027 * can be used as table cell editor in {@link JTable}s. 028 * 029 * Autocompletion is controlled by a list of {@link AutoCompletionListItem}s 030 * managed in a {@link AutoCompletionList}. 031 * 032 * @since 1762 033 */ 034public class AutoCompletingTextField extends JosmTextField implements ComboBoxEditor, TableCellEditor { 035 036 private Integer maxChars; 037 038 /** 039 * The document model for the editor 040 */ 041 class AutoCompletionDocument extends PlainDocument { 042 043 @Override 044 public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { 045 046 // If a maximum number of characters is specified, avoid to exceed it 047 if (maxChars != null && str != null && getLength() + str.length() > maxChars) { 048 int allowedLength = maxChars-getLength(); 049 if (allowedLength > 0) { 050 str = str.substring(0, allowedLength); 051 } else { 052 return; 053 } 054 } 055 056 if (autoCompletionList == null) { 057 super.insertString(offs, str, a); 058 return; 059 } 060 061 // input method for non-latin characters (e.g. scim) 062 if (a != null && a.isDefined(StyleConstants.ComposedTextAttribute)) { 063 super.insertString(offs, str, a); 064 return; 065 } 066 067 // if the current offset isn't at the end of the document we don't autocomplete. 068 // If a highlighted autocompleted suffix was present and we get here Swing has 069 // already removed it from the document. getLength() therefore doesn't include the 070 // autocompleted suffix. 071 // 072 if (offs < getLength()) { 073 super.insertString(offs, str, a); 074 return; 075 } 076 077 String currentText = getText(0, getLength()); 078 // if the text starts with a number we don't autocomplete 079 if (Main.pref.getBoolean("autocomplete.dont_complete_numbers", true)) { 080 try { 081 Long.parseLong(str); 082 if (currentText.length() == 0) { 083 // we don't autocomplete on numbers 084 super.insertString(offs, str, a); 085 return; 086 } 087 Long.parseLong(currentText); 088 super.insertString(offs, str, a); 089 return; 090 } catch(NumberFormatException e) { 091 // either the new text or the current text isn't a number. We continue with 092 // autocompletion 093 } 094 } 095 String prefix = currentText.substring(0, offs); 096 autoCompletionList.applyFilter(prefix+str); 097 if (autoCompletionList.getFilteredSize()>0) { 098 // there are matches. Insert the new text and highlight the 099 // auto completed suffix 100 // 101 String matchingString = autoCompletionList.getFilteredItem(0).getValue(); 102 remove(0,getLength()); 103 super.insertString(0,matchingString,a); 104 105 // highlight from insert position to end position to put the caret at the end 106 setCaretPosition(offs + str.length()); 107 moveCaretPosition(getLength()); 108 } else { 109 // there are no matches. Insert the new text, do not highlight 110 // 111 String newText = prefix + str; 112 remove(0,getLength()); 113 super.insertString(0,newText,a); 114 setCaretPosition(getLength()); 115 116 } 117 } 118 } 119 120 /** the auto completion list user input is matched against */ 121 protected AutoCompletionList autoCompletionList = null; 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 init(); 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 super(columns); 166 init(); 167 } 168 169 protected void applyFilter(String filter) { 170 if (autoCompletionList != null) { 171 autoCompletionList.applyFilter(filter); 172 } 173 } 174 175 /** 176 * Returns the auto completion list. 177 * @return the auto completion list; may be null, if no auto completion list is set 178 */ 179 public AutoCompletionList getAutoCompletionList() { 180 return autoCompletionList; 181 } 182 183 /** 184 * Sets the auto completion list. 185 * @param autoCompletionList the auto completion list; if null, auto completion is 186 * disabled 187 */ 188 public void setAutoCompletionList(AutoCompletionList autoCompletionList) { 189 this.autoCompletionList = autoCompletionList; 190 } 191 192 @Override 193 public Component getEditorComponent() { 194 return this; 195 } 196 197 @Override 198 public Object getItem() { 199 return getText(); 200 } 201 202 @Override 203 public void setItem(Object anObject) { 204 if (anObject == null) { 205 setText(""); 206 } else { 207 setText(anObject.toString()); 208 } 209 } 210 211 /** 212 * Sets the maximum number of characters allowed. 213 * @param max maximum number of characters allowed 214 * @since 5579 215 */ 216 public void setMaxChars(Integer max) { 217 maxChars = max; 218 } 219 220 /* ------------------------------------------------------------------------------------ */ 221 /* TableCellEditor interface */ 222 /* ------------------------------------------------------------------------------------ */ 223 224 private CellEditorSupport tableCellEditorSupport; 225 private String originalValue; 226 227 @Override 228 public void addCellEditorListener(CellEditorListener l) { 229 tableCellEditorSupport.addCellEditorListener(l); 230 } 231 232 protected void rememberOriginalValue(String value) { 233 this.originalValue = value; 234 } 235 236 protected void restoreOriginalValue() { 237 setText(originalValue); 238 } 239 240 @Override 241 public void removeCellEditorListener(CellEditorListener l) { 242 tableCellEditorSupport.removeCellEditorListener(l); 243 } 244 245 @Override 246 public void cancelCellEditing() { 247 restoreOriginalValue(); 248 tableCellEditorSupport.fireEditingCanceled(); 249 } 250 251 @Override 252 public Object getCellEditorValue() { 253 return getText(); 254 } 255 256 @Override 257 public boolean isCellEditable(EventObject anEvent) { 258 return true; 259 } 260 261 @Override 262 public boolean shouldSelectCell(EventObject anEvent) { 263 return true; 264 } 265 266 @Override 267 public boolean stopCellEditing() { 268 tableCellEditorSupport.fireEditingStopped(); 269 return true; 270 } 271 272 @Override 273 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 274 setText( value == null ? "" : value.toString()); 275 rememberOriginalValue(getText()); 276 return this; 277 } 278}