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

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

fix #10470 - fix invalid recognition of items in AutoCompletionComboBox

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