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