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 }