source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletingComboBox.java@ 15835

Last change on this file since 15835 was 15835, checked in by simon04, 4 years ago

fix #14088 - Add tags dialog: use okay button icon from matching preset/style

  • Property svn:eol-style set to native
File size: 16.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.tagging.ac;
3
4import java.awt.Component;
5import java.awt.datatransfer.Clipboard;
6import java.awt.datatransfer.Transferable;
7import java.awt.event.FocusEvent;
8import java.awt.event.FocusListener;
9import java.awt.im.InputContext;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.LinkedList;
13import java.util.Locale;
14
15import javax.swing.ComboBoxEditor;
16import javax.swing.ComboBoxModel;
17import javax.swing.DefaultComboBoxModel;
18import javax.swing.JLabel;
19import javax.swing.JList;
20import javax.swing.ListCellRenderer;
21import javax.swing.text.AttributeSet;
22import javax.swing.text.BadLocationException;
23import javax.swing.text.JTextComponent;
24import javax.swing.text.PlainDocument;
25import javax.swing.text.StyleConstants;
26
27import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
28import org.openstreetmap.josm.data.tagging.ac.AutoCompletionPriority;
29import org.openstreetmap.josm.gui.MainApplication;
30import org.openstreetmap.josm.gui.MapFrame;
31import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
32import org.openstreetmap.josm.gui.widgets.JosmComboBox;
33import org.openstreetmap.josm.spi.preferences.Config;
34import org.openstreetmap.josm.tools.Logging;
35import org.openstreetmap.josm.tools.Utils;
36
37/**
38 * Auto-completing ComboBox.
39 * @author guilhem.bonnefille@gmail.com
40 * @since 272
41 */
42public class AutoCompletingComboBox extends JosmComboBox<AutoCompletionItem> {
43
44 private boolean autocompleteEnabled = true;
45
46 private int maxTextLength = -1;
47 private boolean useFixedLocale;
48
49 private final transient InputContext privateInputContext = InputContext.getInstance();
50
51 static final class InnerFocusListener implements FocusListener {
52 private final JTextComponent editorComponent;
53
54 InnerFocusListener(JTextComponent editorComponent) {
55 this.editorComponent = editorComponent;
56 }
57
58 @Override
59 public void focusLost(FocusEvent e) {
60 MapFrame map = MainApplication.getMap();
61 if (map != null) {
62 map.keyDetector.setEnabled(true);
63 }
64 }
65
66 @Override
67 public void focusGained(FocusEvent e) {
68 MapFrame map = MainApplication.getMap();
69 if (map != null) {
70 map.keyDetector.setEnabled(false);
71 }
72 // save unix system selection (middle mouse paste)
73 Clipboard sysSel = ClipboardUtils.getSystemSelection();
74 if (sysSel != null) {
75 Transferable old = ClipboardUtils.getClipboardContent(sysSel);
76 editorComponent.selectAll();
77 if (old != null) {
78 sysSel.setContents(old, null);
79 }
80 } else if (e != null && e.getOppositeComponent() != null) {
81 // Select all characters when the change of focus occurs inside JOSM only.
82 // When switching from another application, it is annoying, see #13747
83 editorComponent.selectAll();
84 }
85 }
86 }
87
88 /**
89 * Auto-complete a JosmComboBox.
90 * <br>
91 * Inspired by <a href="http://www.orbital-computer.de/JComboBox">Thomas Bierhance example</a>.
92 */
93 class AutoCompletingComboBoxDocument extends PlainDocument {
94 private final JosmComboBox<AutoCompletionItem> comboBox;
95 private boolean selecting;
96
97 /**
98 * Constructs a new {@code AutoCompletingComboBoxDocument}.
99 * @param comboBox the combobox
100 */
101 AutoCompletingComboBoxDocument(final JosmComboBox<AutoCompletionItem> comboBox) {
102 this.comboBox = comboBox;
103 }
104
105 @Override
106 public void remove(int offs, int len) throws BadLocationException {
107 if (selecting)
108 return;
109 try {
110 super.remove(offs, len);
111 } catch (IllegalArgumentException e) {
112 // IAE can happen with Devanagari script, see #15825
113 Logging.error(e);
114 }
115 }
116
117 @Override
118 public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
119 // TODO get rid of code duplication w.r.t. AutoCompletingTextField.AutoCompletionDocument.insertString
120
121 if (selecting || (offs == 0 && str.equals(getText(0, getLength()))))
122 return;
123 if (maxTextLength > -1 && str.length()+getLength() > maxTextLength)
124 return;
125 boolean initial = offs == 0 && getLength() == 0 && str.length() > 1;
126 super.insertString(offs, str, a);
127
128 // return immediately when selecting an item
129 // Note: this is done after calling super method because we need
130 // ActionListener informed
131 if (selecting)
132 return;
133 if (!autocompleteEnabled)
134 return;
135 // input method for non-latin characters (e.g. scim)
136 if (a != null && a.isDefined(StyleConstants.ComposedTextAttribute))
137 return;
138
139 // if the current offset isn't at the end of the document we don't autocomplete.
140 // If a highlighted autocompleted suffix was present and we get here Swing has
141 // already removed it from the document. getLength() therefore doesn't include the autocompleted suffix.
142 if (offs + str.length() < getLength()) {
143 return;
144 }
145
146 int size = getLength();
147 int start = offs+str.length();
148 int end = start;
149 String curText = getText(0, size);
150
151 // item for lookup and selection
152 Object item;
153 // if the text is a number we don't autocomplete
154 if (Config.getPref().getBoolean("autocomplete.dont_complete_numbers", true)) {
155 try {
156 Long.parseLong(str);
157 if (!curText.isEmpty())
158 Long.parseLong(curText);
159 item = lookupItem(curText, true);
160 } catch (NumberFormatException e) {
161 // either the new text or the current text isn't a number. We continue with autocompletion
162 item = lookupItem(curText, false);
163 }
164 } else {
165 item = lookupItem(curText, false);
166 }
167
168 setSelectedItem(item);
169 if (initial) {
170 start = 0;
171 }
172 if (item != null) {
173 String newText = ((AutoCompletionItem) item).getValue();
174 if (!newText.equals(curText)) {
175 selecting = true;
176 super.remove(0, size);
177 super.insertString(0, newText, a);
178 selecting = false;
179 start = size;
180 end = getLength();
181 }
182 }
183 final JTextComponent editorComponent = comboBox.getEditorComponent();
184 // save unix system selection (middle mouse paste)
185 Clipboard sysSel = ClipboardUtils.getSystemSelection();
186 if (sysSel != null) {
187 Transferable old = ClipboardUtils.getClipboardContent(sysSel);
188 editorComponent.select(start, end);
189 if (old != null) {
190 sysSel.setContents(old, null);
191 }
192 } else {
193 editorComponent.select(start, end);
194 }
195 }
196
197 private void setSelectedItem(Object item) {
198 selecting = true;
199 comboBox.setSelectedItem(item);
200 selecting = false;
201 }
202
203 private Object lookupItem(String pattern, boolean match) {
204 ComboBoxModel<AutoCompletionItem> model = comboBox.getModel();
205 AutoCompletionItem bestItem = null;
206 for (int i = 0, n = model.getSize(); i < n; i++) {
207 AutoCompletionItem currentItem = model.getElementAt(i);
208 if (currentItem.getValue().equals(pattern))
209 return currentItem;
210 if (!match && currentItem.getValue().startsWith(pattern)
211 && (bestItem == null || currentItem.getPriority().compareTo(bestItem.getPriority()) > 0)) {
212 bestItem = currentItem;
213 }
214 }
215 return bestItem; // may be null
216 }
217 }
218
219 /**
220 * Creates a <code>AutoCompletingComboBox</code> with a default prototype display value.
221 */
222 public AutoCompletingComboBox() {
223 this("Foo");
224 }
225
226 /**
227 * Creates a <code>AutoCompletingComboBox</code> with the specified prototype display value.
228 * @param prototype the <code>Object</code> used to compute the maximum number of elements to be displayed at once
229 * before displaying a scroll bar. It also affects the initial width of the combo box.
230 * @since 5520
231 */
232 public AutoCompletingComboBox(String prototype) {
233 super(new AutoCompletionItem(prototype));
234 setRenderer(new AutoCompleteListCellRenderer());
235 final JTextComponent editorComponent = this.getEditorComponent();
236 editorComponent.setDocument(new AutoCompletingComboBoxDocument(this));
237 editorComponent.addFocusListener(new InnerFocusListener(editorComponent));
238 }
239
240 /**
241 * Sets the maximum text length.
242 * @param length the maximum text length in number of characters
243 */
244 public void setMaxTextLength(int length) {
245 this.maxTextLength = length;
246 }
247
248 /**
249 * Convert the selected item into a String that can be edited in the editor component.
250 *
251 * @param cbEditor the editor
252 * @param item excepts AutoCompletionListItem, String and null
253 */
254 @Override
255 public void configureEditor(ComboBoxEditor cbEditor, Object item) {
256 if (item == null) {
257 cbEditor.setItem(null);
258 } else if (item instanceof String) {
259 cbEditor.setItem(item);
260 } else if (item instanceof AutoCompletionItem) {
261 cbEditor.setItem(((AutoCompletionItem) item).getValue());
262 } else
263 throw new IllegalArgumentException("Unsupported item: "+item);
264 }
265
266 /**
267 * Selects a given item in the ComboBox model
268 * @param item excepts AutoCompletionItem, String and null
269 */
270 @Override
271 public void setSelectedItem(Object item) {
272 if (item == null) {
273 super.setSelectedItem(null);
274 } else if (item instanceof AutoCompletionItem) {
275 super.setSelectedItem(item);
276 } else if (item instanceof String) {
277 String s = (String) item;
278 // find the string in the model or create a new item
279 for (int i = 0; i < getModel().getSize(); i++) {
280 AutoCompletionItem acItem = getModel().getElementAt(i);
281 if (s.equals(acItem.getValue())) {
282 super.setSelectedItem(acItem);
283 return;
284 }
285 }
286 super.setSelectedItem(new AutoCompletionItem(s, AutoCompletionPriority.UNKNOWN));
287 } else {
288 throw new IllegalArgumentException("Unsupported item: "+item);
289 }
290 }
291
292 /**
293 * Sets the items of the combobox to the given {@code String}s in reversed order (last element first).
294 * @param elems String items
295 */
296 public void setPossibleItems(Collection<String> elems) {
297 DefaultComboBoxModel<AutoCompletionItem> model = (DefaultComboBoxModel<AutoCompletionItem>) this.getModel();
298 Object oldValue = this.getEditor().getItem(); // Do not use getSelectedItem(); (fix #8013)
299 model.removeAllElements();
300 for (String elem : elems) {
301 model.addElement(new AutoCompletionItem(elem, AutoCompletionPriority.UNKNOWN));
302 }
303 this.setSelectedItem(null);
304 // disable autocomplete to prevent unnecessary actions in AutoCompletingComboBoxDocument#insertString
305 autocompleteEnabled = false;
306 this.setSelectedItem(oldValue);
307 autocompleteEnabled = true;
308 }
309
310 /**
311 * Sets the items of the combobox to the given {@code String}s in top down order.
312 * @param elems Collection of String items (is not changed)
313 * @since 15011
314 */
315 public void setPossibleItemsTopDown(Collection<String> elems) {
316 // We have to reverse the history, because ComboBoxHistory will reverse it again in addElement()
317 LinkedList<String> reversed = new LinkedList<>(elems);
318 Collections.reverse(reversed);
319 setPossibleItems(reversed);
320 }
321
322 /**
323 * Sets the items of the combobox to the given {@code AutoCompletionItem}s.
324 * @param elems AutoCompletionItem items
325 * @since 12859
326 */
327 public void setPossibleAcItems(Collection<AutoCompletionItem> elems) {
328 DefaultComboBoxModel<AutoCompletionItem> model = (DefaultComboBoxModel<AutoCompletionItem>) this.getModel();
329 Object oldValue = getSelectedItem();
330 Object editorOldValue = this.getEditor().getItem();
331 model.removeAllElements();
332 for (AutoCompletionItem elem : elems) {
333 model.addElement(elem);
334 }
335 setSelectedItem(oldValue);
336 this.getEditor().setItem(editorOldValue);
337 }
338
339 /**
340 * Determines if autocompletion is enabled.
341 * @return {@code true} if autocompletion is enabled, {@code false} otherwise.
342 */
343 public final boolean isAutocompleteEnabled() {
344 return autocompleteEnabled;
345 }
346
347 /**
348 * Sets whether the autocompletion is enabled
349 * @param autocompleteEnabled {@code true} to enable autocompletion
350 * @since 15567 (visibility)
351 */
352 public void setAutocompleteEnabled(boolean autocompleteEnabled) {
353 this.autocompleteEnabled = autocompleteEnabled;
354 }
355
356 /**
357 * If the locale is fixed, English keyboard layout will be used by default for this combobox
358 * all other components can still have different keyboard layout selected
359 * @param f fixed locale
360 */
361 public void setFixedLocale(boolean f) {
362 useFixedLocale = f;
363 if (useFixedLocale) {
364 Locale oldLocale = privateInputContext.getLocale();
365 Logging.info("Using English input method");
366 if (!privateInputContext.selectInputMethod(new Locale("en", "US"))) {
367 // Unable to use English keyboard layout, disable the feature
368 Logging.warn("Unable to use English input method");
369 useFixedLocale = false;
370 if (oldLocale != null) {
371 Logging.info("Restoring input method to " + oldLocale);
372 if (!privateInputContext.selectInputMethod(oldLocale)) {
373 Logging.warn("Unable to restore input method to " + oldLocale);
374 }
375 }
376 }
377 }
378 }
379
380 @Override
381 public InputContext getInputContext() {
382 if (useFixedLocale) {
383 return privateInputContext;
384 }
385 return super.getInputContext();
386 }
387
388 /**
389 * Returns the edited item with whitespaces removed
390 * @return the edited item with whitespaces removed
391 * @since 15835
392 */
393 public String getEditItem() {
394 return Utils.removeWhiteSpaces(getEditor().getItem().toString());
395 }
396
397 /**
398 * Returns the selected item or the edited item as string
399 * @return the selected item or the edited item as string
400 * @see #getSelectedItem()
401 * @see #getEditItem()
402 * @since 15835
403 */
404 public String getSelectedOrEditItem() {
405 final Object selectedItem = getSelectedItem();
406 if (selectedItem instanceof AutoCompletionItem) {
407 return ((AutoCompletionItem) selectedItem).getValue();
408 } else if (selectedItem instanceof String) {
409 return (String) selectedItem;
410 } else {
411 return getEditItem();
412 }
413 }
414
415 /**
416 * ListCellRenderer for AutoCompletingComboBox
417 * renders an AutoCompletionListItem by showing only the string value part
418 */
419 public static class AutoCompleteListCellRenderer extends JLabel implements ListCellRenderer<AutoCompletionItem> {
420
421 /**
422 * Constructs a new {@code AutoCompleteListCellRenderer}.
423 */
424 public AutoCompleteListCellRenderer() {
425 setOpaque(true);
426 }
427
428 @Override
429 public Component getListCellRendererComponent(
430 JList<? extends AutoCompletionItem> list,
431 AutoCompletionItem item,
432 int index,
433 boolean isSelected,
434 boolean cellHasFocus) {
435 if (isSelected) {
436 setBackground(list.getSelectionBackground());
437 setForeground(list.getSelectionForeground());
438 } else {
439 setBackground(list.getBackground());
440 setForeground(list.getForeground());
441 }
442
443 setText(item.getValue());
444 return this;
445 }
446 }
447}
Note: See TracBrowser for help on using the repository browser.