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