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