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