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