001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.tagging.ac; 003 004import java.awt.Component; 005import java.awt.Toolkit; 006import java.awt.datatransfer.Clipboard; 007import java.awt.datatransfer.Transferable; 008import java.awt.event.FocusEvent; 009import java.awt.event.FocusListener; 010import java.awt.im.InputContext; 011import java.util.Collection; 012import java.util.Locale; 013 014import javax.swing.ComboBoxEditor; 015import javax.swing.ComboBoxModel; 016import javax.swing.DefaultComboBoxModel; 017import javax.swing.JLabel; 018import javax.swing.JList; 019import javax.swing.ListCellRenderer; 020import javax.swing.text.AttributeSet; 021import javax.swing.text.BadLocationException; 022import javax.swing.text.JTextComponent; 023import javax.swing.text.PlainDocument; 024import javax.swing.text.StyleConstants; 025 026import org.openstreetmap.josm.Main; 027import org.openstreetmap.josm.gui.widgets.JosmComboBox; 028 029/** 030 * @author guilhem.bonnefille@gmail.com 031 */ 032public class AutoCompletingComboBox extends JosmComboBox { 033 034 private boolean autocompleteEnabled = true; 035 036 private int maxTextLength = -1; 037 private boolean useFixedLocale; 038 039 /** 040 * Auto-complete a JosmComboBox. 041 * 042 * Inspired by http://www.orbital-computer.de/JComboBox/ 043 */ 044 class AutoCompletingComboBoxDocument extends PlainDocument { 045 private JosmComboBox comboBox; 046 private boolean selecting = false; 047 048 public AutoCompletingComboBoxDocument(final JosmComboBox comboBox) { 049 this.comboBox = comboBox; 050 } 051 052 @Override public void remove(int offs, int len) throws BadLocationException { 053 if (selecting) 054 return; 055 super.remove(offs, len); 056 } 057 058 @Override public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { 059 if (selecting || (offs == 0 && str.equals(getText(0, getLength())))) 060 return; 061 if (maxTextLength > -1 && str.length()+getLength() > maxTextLength) 062 return; 063 boolean initial = (offs == 0 && getLength() == 0 && str.length() > 1); 064 super.insertString(offs, str, a); 065 066 // return immediately when selecting an item 067 // Note: this is done after calling super method because we need 068 // ActionListener informed 069 if (selecting) 070 return; 071 if (!autocompleteEnabled) 072 return; 073 // input method for non-latin characters (e.g. scim) 074 if (a != null && a.isDefined(StyleConstants.ComposedTextAttribute)) 075 return; 076 077 int size = getLength(); 078 int start = offs+str.length(); 079 int end = start; 080 String curText = getText(0, size); 081 082 // item for lookup and selection 083 Object item = null; 084 // if the text is a number we don't autocomplete 085 if (Main.pref.getBoolean("autocomplete.dont_complete_numbers", true)) { 086 try { 087 Long.parseLong(str); 088 if (curText.length() != 0) 089 Long.parseLong(curText); 090 item = lookupItem(curText, true); 091 } catch (NumberFormatException e) { 092 // either the new text or the current text isn't a number. We continue with 093 // autocompletion 094 item = lookupItem(curText, false); 095 } 096 } else { 097 item = lookupItem(curText, false); 098 } 099 100 setSelectedItem(item); 101 if (initial) { 102 start = 0; 103 } 104 if (item != null) { 105 String newText = ((AutoCompletionListItem) item).getValue(); 106 if (!newText.equals(curText)) 107 { 108 selecting = true; 109 super.remove(0, size); 110 super.insertString(0, newText, a); 111 selecting = false; 112 start = size; 113 end = getLength(); 114 } 115 } 116 JTextComponent editor = (JTextComponent)comboBox.getEditor().getEditorComponent(); 117 // save unix system selection (middle mouse paste) 118 Clipboard sysSel = Toolkit.getDefaultToolkit().getSystemSelection(); 119 if(sysSel != null) { 120 Transferable old = sysSel.getContents(null); 121 editor.select(start, end); 122 sysSel.setContents(old, null); 123 } else { 124 editor.select(start, end); 125 } 126 } 127 128 private void setSelectedItem(Object item) { 129 selecting = true; 130 comboBox.setSelectedItem(item); 131 selecting = false; 132 } 133 134 private Object lookupItem(String pattern, boolean match) { 135 ComboBoxModel model = comboBox.getModel(); 136 AutoCompletionListItem bestItem = null; 137 for (int i = 0, n = model.getSize(); i < n; i++) { 138 AutoCompletionListItem currentItem = (AutoCompletionListItem) model.getElementAt(i); 139 if (currentItem.getValue().equals(pattern)) 140 return currentItem; 141 if (!match && currentItem.getValue().startsWith(pattern)) { 142 if (bestItem == null || currentItem.getPriority().compareTo(bestItem.getPriority()) > 0) { 143 bestItem = currentItem; 144 } 145 } 146 } 147 return bestItem; // may be null 148 } 149 } 150 151 /** 152 * Creates a <code>AutoCompletingComboBox</code> with a default prototype display value. 153 */ 154 public AutoCompletingComboBox() { 155 this(JosmComboBox.DEFAULT_PROTOTYPE_DISPLAY_VALUE); 156 } 157 158 /** 159 * Creates a <code>AutoCompletingComboBox</code> with the specified prototype display value. 160 * @param prototype the <code>Object</code> used to compute the maximum number of elements to be displayed at once before displaying a scroll bar. 161 * It also affects the initial width of the combo box. 162 * @since 5520 163 */ 164 public AutoCompletingComboBox(String prototype) { 165 super(new AutoCompletionListItem(prototype)); 166 setRenderer(new AutoCompleteListCellRenderer()); 167 final JTextComponent editor = (JTextComponent) this.getEditor().getEditorComponent(); 168 editor.setDocument(new AutoCompletingComboBoxDocument(this)); 169 editor.addFocusListener( 170 new FocusListener() { 171 @Override 172 public void focusLost(FocusEvent e) { 173 } 174 @Override 175 public void focusGained(FocusEvent e) { 176 // save unix system selection (middle mouse paste) 177 Clipboard sysSel = Toolkit.getDefaultToolkit().getSystemSelection(); 178 if(sysSel != null) { 179 Transferable old = sysSel.getContents(null); 180 editor.selectAll(); 181 sysSel.setContents(old, null); 182 } else { 183 editor.selectAll(); 184 } 185 } 186 } 187 ); 188 } 189 190 public void setMaxTextLength(int length) 191 { 192 this.maxTextLength = length; 193 } 194 195 /** 196 * Convert the selected item into a String 197 * that can be edited in the editor component. 198 * 199 * @param editor the editor 200 * @param item excepts AutoCompletionListItem, String and null 201 */ 202 @Override public void configureEditor(ComboBoxEditor editor, Object item) { 203 if (item == null) { 204 editor.setItem(null); 205 } else if (item instanceof String) { 206 editor.setItem(item); 207 } else if (item instanceof AutoCompletionListItem) { 208 editor.setItem(((AutoCompletionListItem)item).getValue()); 209 } else 210 throw new IllegalArgumentException(); 211 } 212 213 /** 214 * Selects a given item in the ComboBox model 215 * @param item excepts AutoCompletionListItem, String and null 216 */ 217 @Override public void setSelectedItem(Object item) { 218 if (item == null) { 219 super.setSelectedItem(null); 220 } else if (item instanceof AutoCompletionListItem) { 221 super.setSelectedItem(item); 222 } else if (item instanceof String) { 223 String s = (String) item; 224 // find the string in the model or create a new item 225 for (int i=0; i< getModel().getSize(); i++) { 226 AutoCompletionListItem acItem = (AutoCompletionListItem) getModel().getElementAt(i); 227 if (s.equals(acItem.getValue())) { 228 super.setSelectedItem(acItem); 229 return; 230 } 231 } 232 super.setSelectedItem(new AutoCompletionListItem(s, AutoCompletionItemPriority.UNKNOWN)); 233 } else 234 throw new IllegalArgumentException(); 235 } 236 237 /** 238 * sets the items of the combobox to the given strings 239 */ 240 public void setPossibleItems(Collection<String> elems) { 241 DefaultComboBoxModel model = (DefaultComboBoxModel)this.getModel(); 242 Object oldValue = this.getEditor().getItem(); // Do not use getSelectedItem(); (fix #8013) 243 model.removeAllElements(); 244 for (String elem : elems) { 245 model.addElement(new AutoCompletionListItem(elem, AutoCompletionItemPriority.UNKNOWN)); 246 } 247 // disable autocomplete to prevent unnecessary actions in 248 // AutoCompletingComboBoxDocument#insertString 249 autocompleteEnabled = false; 250 this.getEditor().setItem(oldValue); // Do not use setSelectedItem(oldValue); (fix #8013) 251 autocompleteEnabled = true; 252 } 253 254 /** 255 * sets the items of the combobox to the given AutoCompletionListItems 256 */ 257 public void setPossibleACItems(Collection<AutoCompletionListItem> elems) { 258 DefaultComboBoxModel model = (DefaultComboBoxModel)this.getModel(); 259 Object oldValue = getSelectedItem(); 260 Object editorOldValue = this.getEditor().getItem(); 261 model.removeAllElements(); 262 for (AutoCompletionListItem elem : elems) { 263 model.addElement(elem); 264 } 265 setSelectedItem(oldValue); 266 this.getEditor().setItem(editorOldValue); 267 } 268 269 270 protected boolean isAutocompleteEnabled() { 271 return autocompleteEnabled; 272 } 273 274 protected void setAutocompleteEnabled(boolean autocompleteEnabled) { 275 this.autocompleteEnabled = autocompleteEnabled; 276 } 277 278 /** 279 * If the locale is fixed, English keyboard layout will be used by default for this combobox 280 * all other components can still have different keyboard layout selected 281 */ 282 public void setFixedLocale(boolean f) { 283 useFixedLocale = f; 284 if (useFixedLocale) { 285 privateInputContext.selectInputMethod(new Locale("en", "US")); 286 } 287 } 288 289 private static InputContext privateInputContext = InputContext.getInstance(); 290 291 @Override 292 public InputContext getInputContext() { 293 if (useFixedLocale) { 294 return privateInputContext; 295 } 296 return super.getInputContext(); 297 } 298 299 /** 300 * ListCellRenderer for AutoCompletingComboBox 301 * renders an AutoCompletionListItem by showing only the string value part 302 */ 303 public static class AutoCompleteListCellRenderer extends JLabel implements ListCellRenderer { 304 305 public AutoCompleteListCellRenderer() { 306 setOpaque(true); 307 } 308 309 @Override 310 public Component getListCellRendererComponent( 311 JList list, 312 Object value, 313 int index, 314 boolean isSelected, 315 boolean cellHasFocus) 316 { 317 if (isSelected) { 318 setBackground(list.getSelectionBackground()); 319 setForeground(list.getSelectionForeground()); 320 } else { 321 setBackground(list.getBackground()); 322 setForeground(list.getForeground()); 323 } 324 325 AutoCompletionListItem item = (AutoCompletionListItem) value; 326 setText(item.getValue()); 327 return this; 328 } 329 } 330}