001 // License: GPL. Copyright 2007 by Immanuel Scholz and others
002 package org.openstreetmap.josm.gui.tagging.ac;
003
004 import java.awt.Component;
005 import java.awt.Toolkit;
006 import java.awt.datatransfer.Clipboard;
007 import java.awt.datatransfer.Transferable;
008 import java.awt.event.FocusEvent;
009 import java.awt.event.FocusListener;
010 import java.util.Collection;
011
012 import javax.swing.ComboBoxEditor;
013 import javax.swing.ComboBoxModel;
014 import javax.swing.DefaultComboBoxModel;
015 import javax.swing.JLabel;
016 import javax.swing.JList;
017 import javax.swing.ListCellRenderer;
018 import javax.swing.text.AttributeSet;
019 import javax.swing.text.BadLocationException;
020 import javax.swing.text.JTextComponent;
021 import javax.swing.text.PlainDocument;
022 import javax.swing.text.StyleConstants;
023
024 import org.openstreetmap.josm.Main;
025 import org.openstreetmap.josm.gui.widgets.JosmComboBox;
026
027 /**
028 * @author guilhem.bonnefille@gmail.com
029 */
030 public class AutoCompletingComboBox extends JosmComboBox {
031
032 private boolean autocompleteEnabled = true;
033
034 private int maxTextLength = -1;
035
036 /**
037 * Auto-complete a JosmComboBox.
038 *
039 * Inspired by http://www.orbital-computer.de/JComboBox/
040 */
041 class AutoCompletingComboBoxDocument extends PlainDocument {
042 private JosmComboBox comboBox;
043 private boolean selecting = false;
044
045 public AutoCompletingComboBoxDocument(final JosmComboBox comboBox) {
046 this.comboBox = comboBox;
047 }
048
049 @Override public void remove(int offs, int len) throws BadLocationException {
050 if (selecting)
051 return;
052 super.remove(offs, len);
053 }
054
055 @Override public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
056 if (selecting || (offs == 0 && str.equals(getText(0, getLength()))))
057 return;
058 if (maxTextLength > -1 && str.length()+getLength() > maxTextLength)
059 return;
060 boolean initial = (offs == 0 && getLength() == 0 && str.length() > 1);
061 super.insertString(offs, str, a);
062
063 // return immediately when selecting an item
064 // Note: this is done after calling super method because we need
065 // ActionListener informed
066 if (selecting)
067 return;
068 if (!autocompleteEnabled)
069 return;
070 // input method for non-latin characters (e.g. scim)
071 if (a != null && a.isDefined(StyleConstants.ComposedTextAttribute))
072 return;
073
074 int size = getLength();
075 int start = offs+str.length();
076 int end = start;
077 String curText = getText(0, size);
078
079 // item for lookup and selection
080 Object item = null;
081 // if the text is a number we don't autocomplete
082 if (Main.pref.getBoolean("autocomplete.dont_complete_numbers", true)) {
083 try {
084 Long.parseLong(str);
085 if (curText.length() != 0)
086 Long.parseLong(curText);
087 item = lookupItem(curText, true);
088 } catch (NumberFormatException e) {
089 // either the new text or the current text isn't a number. We continue with
090 // autocompletion
091 item = lookupItem(curText, false);
092 }
093 } else {
094 item = lookupItem(curText, false);
095 }
096
097 setSelectedItem(item);
098 if (initial) {
099 start = 0;
100 }
101 if (item != null) {
102 String newText = ((AutoCompletionListItem) item).getValue();
103 if (!newText.equals(curText))
104 {
105 selecting = true;
106 super.remove(0, size);
107 super.insertString(0, newText, a);
108 selecting = false;
109 start = size;
110 end = getLength();
111 }
112 }
113 JTextComponent editor = (JTextComponent)comboBox.getEditor().getEditorComponent();
114 // save unix system selection (middle mouse paste)
115 Clipboard sysSel = Toolkit.getDefaultToolkit().getSystemSelection();
116 if(sysSel != null) {
117 Transferable old = sysSel.getContents(null);
118 editor.select(start, end);
119 sysSel.setContents(old, null);
120 } else {
121 editor.select(start, end);
122 }
123 }
124
125 private void setSelectedItem(Object item) {
126 selecting = true;
127 comboBox.setSelectedItem(item);
128 selecting = false;
129 }
130
131 private Object lookupItem(String pattern, boolean match) {
132 ComboBoxModel model = comboBox.getModel();
133 AutoCompletionListItem bestItem = null;
134 for (int i = 0, n = model.getSize(); i < n; i++) {
135 AutoCompletionListItem currentItem = (AutoCompletionListItem) model.getElementAt(i);
136 if (currentItem.getValue().equals(pattern))
137 return currentItem;
138 if (!match && currentItem.getValue().startsWith(pattern)) {
139 if (bestItem == null || currentItem.getPriority().compareTo(bestItem.getPriority()) > 0) {
140 bestItem = currentItem;
141 }
142 }
143 }
144 return bestItem; // may be null
145 }
146 }
147
148 /**
149 * Creates a <code>AutoCompletingComboBox</code> with a default prototype display value.
150 */
151 public AutoCompletingComboBox() {
152 this(JosmComboBox.DEFAULT_PROTOTYPE_DISPLAY_VALUE);
153 }
154
155 /**
156 * Creates a <code>AutoCompletingComboBox</code> with the specified prototype display value.
157 * @param prototype the <code>Object</code> used to compute the maximum number of elements to be displayed at once before displaying a scroll bar.
158 * It also affects the initial width of the combo box.
159 * @since 5520
160 */
161 public AutoCompletingComboBox(String prototype) {
162 super(new AutoCompletionListItem(prototype));
163 setRenderer(new AutoCompleteListCellRenderer());
164 final JTextComponent editor = (JTextComponent) this.getEditor().getEditorComponent();
165 editor.setDocument(new AutoCompletingComboBoxDocument(this));
166 editor.addFocusListener(
167 new FocusListener() {
168 public void focusLost(FocusEvent e) {
169 }
170 public void focusGained(FocusEvent e) {
171 // save unix system selection (middle mouse paste)
172 Clipboard sysSel = Toolkit.getDefaultToolkit().getSystemSelection();
173 if(sysSel != null) {
174 Transferable old = sysSel.getContents(null);
175 editor.selectAll();
176 sysSel.setContents(old, null);
177 } else {
178 editor.selectAll();
179 }
180 }
181 }
182 );
183 }
184
185 public void setMaxTextLength(int length)
186 {
187 this.maxTextLength = length;
188 }
189
190 /**
191 * Convert the selected item into a String
192 * that can be edited in the editor component.
193 *
194 * @param editor the editor
195 * @param item excepts AutoCompletionListItem, String and null
196 */
197 @Override public void configureEditor(ComboBoxEditor editor, Object item) {
198 if (item == null) {
199 editor.setItem(null);
200 } else if (item instanceof String) {
201 editor.setItem(item);
202 } else if (item instanceof AutoCompletionListItem) {
203 editor.setItem(((AutoCompletionListItem)item).getValue());
204 } else
205 throw new IllegalArgumentException();
206 }
207
208 /**
209 * Selects a given item in the ComboBox model
210 * @param item excepts AutoCompletionListItem, String and null
211 */
212 @Override public void setSelectedItem(Object item) {
213 if (item == null) {
214 super.setSelectedItem(null);
215 } else if (item instanceof AutoCompletionListItem) {
216 super.setSelectedItem(item);
217 } else if (item instanceof String) {
218 String s = (String) item;
219 // find the string in the model or create a new item
220 for (int i=0; i< getModel().getSize(); i++) {
221 AutoCompletionListItem acItem = (AutoCompletionListItem) getModel().getElementAt(i);
222 if (s.equals(acItem.getValue())) {
223 super.setSelectedItem(acItem);
224 return;
225 }
226 }
227 super.setSelectedItem(new AutoCompletionListItem(s, AutoCompletionItemPritority.UNKNOWN));
228 } else
229 throw new IllegalArgumentException();
230 }
231
232 /**
233 * sets the items of the combobox to the given strings
234 */
235 public void setPossibleItems(Collection<String> elems) {
236 DefaultComboBoxModel model = (DefaultComboBoxModel)this.getModel();
237 Object oldValue = this.getEditor().getItem(); // Do not use getSelectedItem(); (fix #8013)
238 model.removeAllElements();
239 for (String elem : elems) {
240 model.addElement(new AutoCompletionListItem(elem, AutoCompletionItemPritority.UNKNOWN));
241 }
242 // disable autocomplete to prevent unnecessary actions in
243 // AutoCompletingComboBoxDocument#insertString
244 autocompleteEnabled = false;
245 this.getEditor().setItem(oldValue); // Do not use setSelectedItem(oldValue); (fix #8013)
246 autocompleteEnabled = true;
247 }
248
249 /**
250 * sets the items of the combobox to the given AutoCompletionListItems
251 */
252 public void setPossibleACItems(Collection<AutoCompletionListItem> elems) {
253 DefaultComboBoxModel model = (DefaultComboBoxModel)this.getModel();
254 Object oldValue = getSelectedItem();
255 Object editorOldValue = this.getEditor().getItem();
256 model.removeAllElements();
257 for (AutoCompletionListItem elem : elems) {
258 model.addElement(elem);
259 }
260 setSelectedItem(oldValue);
261 this.getEditor().setItem(editorOldValue);
262 }
263
264
265 protected boolean isAutocompleteEnabled() {
266 return autocompleteEnabled;
267 }
268
269 protected void setAutocompleteEnabled(boolean autocompleteEnabled) {
270 this.autocompleteEnabled = autocompleteEnabled;
271 }
272
273 /**
274 * ListCellRenderer for AutoCompletingComboBox
275 * renders an AutoCompletionListItem by showing only the string value part
276 */
277 public static class AutoCompleteListCellRenderer extends JLabel implements ListCellRenderer {
278
279 public AutoCompleteListCellRenderer() {
280 setOpaque(true);
281 }
282
283 public Component getListCellRendererComponent(
284 JList list,
285 Object value,
286 int index,
287 boolean isSelected,
288 boolean cellHasFocus)
289 {
290 if (isSelected) {
291 setBackground(list.getSelectionBackground());
292 setForeground(list.getSelectionForeground());
293 } else {
294 setBackground(list.getBackground());
295 setForeground(list.getForeground());
296 }
297
298 AutoCompletionListItem item = (AutoCompletionListItem) value;
299 setText(item.getValue());
300 return this;
301 }
302 }
303 }