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

Last change on this file since 13121 was 12859, checked in by Don-vip, 7 years ago

see #15229 - see #15182 - deprecate non-GUI AutoCompletion* classes from gui.tagging.ac. Offer better replacements in new data.tagging.ac package

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