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

Last change on this file since 13139 was 13139, checked in by Don-vip, 6 years ago

fix #13747 - do not select all characters from a combobox when switching between JOSM and another application

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