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

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

see #15310 - remove most of deprecated APIs

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