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

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

fix #15825 - catch IAE to add robustness with Devanagari script

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