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