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 public AutoCompletingComboBox() { 149 super(new AutoCompletionListItem(JosmComboBox.DEFAULT_PROTOTYPE_DISPLAY_VALUE)); 150 setRenderer(new AutoCompleteListCellRenderer()); 151 final JTextComponent editor = (JTextComponent) this.getEditor().getEditorComponent(); 152 editor.setDocument(new AutoCompletingComboBoxDocument(this)); 153 editor.addFocusListener( 154 new FocusListener() { 155 public void focusLost(FocusEvent e) { 156 } 157 public void focusGained(FocusEvent e) { 158 // save unix system selection (middle mouse paste) 159 Clipboard sysSel = Toolkit.getDefaultToolkit().getSystemSelection(); 160 if(sysSel != null) { 161 Transferable old = sysSel.getContents(null); 162 editor.selectAll(); 163 sysSel.setContents(old, null); 164 } else { 165 editor.selectAll(); 166 } 167 } 168 } 169 ); 170 } 171 172 public void setMaxTextLength(int length) 173 { 174 this.maxTextLength = length; 175 } 176 177 /** 178 * Convert the selected item into a String 179 * that can be edited in the editor component. 180 * 181 * @param editor the editor 182 * @param item excepts AutoCompletionListItem, String and null 183 */ 184 @Override public void configureEditor(ComboBoxEditor editor, Object item) { 185 if (item == null) { 186 editor.setItem(null); 187 } else if (item instanceof String) { 188 editor.setItem(item); 189 } else if (item instanceof AutoCompletionListItem) { 190 editor.setItem(((AutoCompletionListItem)item).getValue()); 191 } else 192 throw new IllegalArgumentException(); 193 } 194 195 /** 196 * Selects a given item in the ComboBox model 197 * @param item excepts AutoCompletionListItem, String and null 198 */ 199 @Override public void setSelectedItem(Object item) { 200 if (item == null) { 201 super.setSelectedItem(null); 202 } else if (item instanceof AutoCompletionListItem) { 203 super.setSelectedItem(item); 204 } else if (item instanceof String) { 205 String s = (String) item; 206 // find the string in the model or create a new item 207 for (int i=0; i< getModel().getSize(); i++) { 208 AutoCompletionListItem acItem = (AutoCompletionListItem) getModel().getElementAt(i); 209 if (s.equals(acItem.getValue())) { 210 super.setSelectedItem(acItem); 211 return; 212 } 213 } 214 super.setSelectedItem(new AutoCompletionListItem(s, AutoCompletionItemPritority.UNKNOWN)); 215 } else 216 throw new IllegalArgumentException(); 217 } 218 219 /** 220 * sets the items of the combobox to the given strings 221 */ 222 public void setPossibleItems(Collection<String> elems) { 223 DefaultComboBoxModel model = (DefaultComboBoxModel)this.getModel(); 224 Object oldValue = this.getEditor().getItem(); // Do not use getSelectedItem(); (fix #8013) 225 model.removeAllElements(); 226 for (String elem : elems) { 227 model.addElement(new AutoCompletionListItem(elem, AutoCompletionItemPritority.UNKNOWN)); 228 } 229 // disable autocomplete to prevent unnecessary actions in 230 // AutoCompletingComboBoxDocument#insertString 231 autocompleteEnabled = false; 232 this.getEditor().setItem(oldValue); // Do not use setSelectedItem(oldValue); (fix #8013) 233 autocompleteEnabled = true; 234 } 235 236 /** 237 * sets the items of the combobox to the given AutoCompletionListItems 238 */ 239 public void setPossibleACItems(Collection<AutoCompletionListItem> elems) { 240 DefaultComboBoxModel model = (DefaultComboBoxModel)this.getModel(); 241 Object oldValue = getSelectedItem(); 242 Object editorOldValue = this.getEditor().getItem(); 243 model.removeAllElements(); 244 for (AutoCompletionListItem elem : elems) { 245 model.addElement(elem); 246 } 247 setSelectedItem(oldValue); 248 this.getEditor().setItem(editorOldValue); 249 } 250 251 252 protected boolean isAutocompleteEnabled() { 253 return autocompleteEnabled; 254 } 255 256 protected void setAutocompleteEnabled(boolean autocompleteEnabled) { 257 this.autocompleteEnabled = autocompleteEnabled; 258 } 259 260 /** 261 * ListCellRenderer for AutoCompletingComboBox 262 * renders an AutoCompletionListItem by showing only the string value part 263 */ 264 public static class AutoCompleteListCellRenderer extends JLabel implements ListCellRenderer { 265 266 public AutoCompleteListCellRenderer() { 267 setOpaque(true); 268 } 269 270 public Component getListCellRendererComponent( 271 JList list, 272 Object value, 273 int index, 274 boolean isSelected, 275 boolean cellHasFocus) 276 { 277 if (isSelected) { 278 setBackground(list.getSelectionBackground()); 279 setForeground(list.getSelectionForeground()); 280 } else { 281 setBackground(list.getBackground()); 282 setForeground(list.getForeground()); 283 } 284 285 AutoCompletionListItem item = (AutoCompletionListItem) value; 286 setText(item.getValue()); 287 return this; 288 } 289 } 290 }