001/* 002 * $Id: AutoCompleteDecorator.java 4051 2011-07-19 20:17:05Z kschaefe $ 003 * 004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, 005 * Santa Clara, California 95054, U.S.A. All rights reserved. 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this library; if not, write to the Free Software 019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 020 */ 021package org.jdesktop.swingx.autocomplete; 022 023import static java.util.Arrays.asList; 024import static java.util.Collections.unmodifiableList; 025 026import java.awt.event.ActionEvent; 027import java.awt.event.ActionListener; 028import java.awt.event.FocusListener; 029import java.awt.event.KeyListener; 030import java.beans.PropertyChangeListener; 031import java.util.List; 032 033import javax.swing.Action; 034import javax.swing.ActionMap; 035import javax.swing.InputMap; 036import javax.swing.JComboBox; 037import javax.swing.JList; 038import javax.swing.KeyStroke; 039import javax.swing.UIManager; 040import javax.swing.event.ListSelectionListener; 041import javax.swing.plaf.UIResource; 042import javax.swing.text.DefaultEditorKit; 043import javax.swing.text.Document; 044import javax.swing.text.JTextComponent; 045import javax.swing.text.StyledDocument; 046import javax.swing.text.TextAction; 047 048import org.jdesktop.swingx.autocomplete.workarounds.MacOSXPopupLocationFix; 049 050/** 051 * This class contains only static utility methods that can be used to set up 052 * automatic completion for some Swing components. 053 * <p>Usage examples:</p> 054 * <p><pre><code> 055 * JComboBox comboBox = [...]; 056 * AutoCompleteDecorator.<b>decorate</b>(comboBox); 057 * 058 * List items = [...]; 059 * JTextField textField = [...]; 060 * AutoCompleteDecorator.<b>decorate</b>(textField, items); 061 * 062 * JList list = [...]; 063 * JTextField textField = [...]; 064 * AutoCompleteDecorator.<b>decorate</b>(list, textField); 065 * </code></pre></p> 066 * 067 * @author Thomas Bierhance 068 * @author Karl Schaefer 069 */ 070@SuppressWarnings({"nls", "serial"}) 071public class AutoCompleteDecorator { 072 //these keys were pulled from BasicComboBoxUI from Sun JDK 1.6.0_20 073 private static final List<String> COMBO_BOX_ACTIONS = unmodifiableList(asList("selectNext", 074 "selectNext2", "selectPrevious", "selectPrevious2", "pageDownPassThrough", 075 "pageUpPassThrough", "homePassThrough", "endPassThrough")); 076 /** 077 * A TextAction that provides an error feedback for the text component that invoked 078 * the action. The error feedback is most likely a "beep". 079 */ 080 private static final Object errorFeedbackAction = new TextAction("provide-error-feedback") { 081 @Override 082 public void actionPerformed(ActionEvent e) { 083 UIManager.getLookAndFeel().provideErrorFeedback(getTextComponent(e)); 084 } 085 }; 086 087 private AutoCompleteDecorator() { 088 //prevents instantiation 089 } 090 091 private static void installMap(InputMap componentMap, boolean strict) { 092 InputMap map = new AutoComplete.InputMap(); 093 094 if (strict) { 095 map.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_BACK_SPACE, 0), DefaultEditorKit.selectionBackwardAction); 096 // ignore VK_DELETE and CTRL+VK_X and beep instead when strict matching 097 map.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_DELETE, 0), errorFeedbackAction); 098 map.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_X, java.awt.event.InputEvent.CTRL_DOWN_MASK), errorFeedbackAction); 099 } else { 100 // VK_BACKSPACE will move the selection to the left if the selected item is in the list 101 // it will delete the previous character otherwise 102 map.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_BACK_SPACE, 0), "nonstrict-backspace"); 103 // leave VK_DELETE and CTRL+VK_X as is 104 } 105 106 map.setParent(componentMap.getParent()); 107 componentMap.setParent(map); 108 } 109 110 static AutoCompleteDocument createAutoCompleteDocument( 111 AbstractAutoCompleteAdaptor adaptor, boolean strictMatching, 112 ObjectToStringConverter stringConverter, Document delegate) { 113 if (delegate instanceof StyledDocument) { 114 return new AutoCompleteStyledDocument(adaptor, strictMatching, 115 stringConverter, (StyledDocument) delegate); 116 } 117 118 return new AutoCompleteDocument(adaptor, strictMatching, 119 stringConverter, delegate); 120 } 121 122 /** 123 * Enables automatic completion for the given JComboBox. The automatic 124 * completion will be strict (only items from the combo box can be selected) 125 * if the combo box is not editable. 126 * @param comboBox a combo box 127 * @see #decorate(JComboBox, ObjectToStringConverter) 128 */ 129 public static void decorate(JComboBox comboBox) { 130 decorate(comboBox, null); 131 } 132 133 /** 134 * Enables automatic completion for the given JComboBox. The automatic 135 * completion will be strict (only items from the combo box can be selected) 136 * if the combo box is not editable. 137 * <p> 138 * <b>Note:</b> the {@code AutoCompleteDecorator} will alter the state of 139 * the {@code JComboBox} to be editable. This can cause side effects with 140 * layouts and sizing. {@code JComboBox} caches the size, which differs 141 * depending on the component's editability. Therefore, if the component's 142 * size is accessed prior to being decorated and then the cached size is 143 * forced to be recalculated, the size of the component will change. 144 * <p> 145 * Because the size of the component can be altered (recalculated), the 146 * decorator does not attempt to set any sizes on the supplied 147 * {@code JComboBox}. Users that need to ensure sizes of supplied combos 148 * should take measures to set the size of the combo. 149 * 150 * @param comboBox 151 * a combo box 152 * @param stringConverter 153 * the converter used to transform items to strings 154 */ 155 public static void decorate(JComboBox comboBox, ObjectToStringConverter stringConverter) { 156 undecorate(comboBox); 157 158 boolean strictMatching = !comboBox.isEditable(); 159 // has to be editable 160 comboBox.setEditable(true); 161 // fix the popup location 162 MacOSXPopupLocationFix.install(comboBox); 163 164 // configure the text component=editor component 165 JTextComponent editorComponent = (JTextComponent) comboBox.getEditor().getEditorComponent(); 166 final AbstractAutoCompleteAdaptor adaptor = new ComboBoxAdaptor(comboBox); 167 final AutoCompleteDocument document = createAutoCompleteDocument(adaptor, strictMatching, 168 stringConverter, editorComponent.getDocument()); 169 decorate(editorComponent, document, adaptor); 170 171 editorComponent.addKeyListener(new AutoComplete.KeyAdapter(comboBox)); 172 173 //set before adding the listener for the editor 174 comboBox.setEditor(new AutoCompleteComboBoxEditor(comboBox.getEditor(), document.stringConverter)); 175 176 // Changing the l&f can change the combobox' editor which in turn 177 // would not be autocompletion-enabled. The new editor needs to be set-up. 178 AutoComplete.PropertyChangeListener pcl = new AutoComplete.PropertyChangeListener(comboBox); 179 comboBox.addPropertyChangeListener("editor", pcl); 180 comboBox.addPropertyChangeListener("enabled", pcl); 181 182 if (!strictMatching) { 183 ActionMap map = comboBox.getActionMap(); 184 185 for (String key : COMBO_BOX_ACTIONS) { 186 Action a = map.get(key); 187 map.put(key, new AutoComplete.SelectionAction(a)); 188 } 189 } 190 } 191 192 static void undecorate(JComboBox comboBox) { 193 JTextComponent editorComponent = (JTextComponent) comboBox.getEditor().getEditorComponent(); 194 195 if (editorComponent.getDocument() instanceof AutoCompleteDocument) { 196 AutoCompleteDocument doc = (AutoCompleteDocument) editorComponent.getDocument(); 197 198 if (doc.strictMatching) { 199 ActionMap map = comboBox.getActionMap(); 200 201 for (String key : COMBO_BOX_ACTIONS) { 202 map.put(key, null); 203 } 204 } 205 206 //remove old property change listener 207 for (PropertyChangeListener l : comboBox.getPropertyChangeListeners("editor")) { 208 if (l instanceof AutoComplete.PropertyChangeListener) { 209 comboBox.removePropertyChangeListener("editor", l); 210 } 211 } 212 213 for (PropertyChangeListener l : comboBox.getPropertyChangeListeners("enabled")) { 214 if (l instanceof AutoComplete.PropertyChangeListener) { 215 comboBox.removePropertyChangeListener("enabled", l); 216 } 217 } 218 219 AutoCompleteComboBoxEditor editor = (AutoCompleteComboBoxEditor) comboBox.getEditor(); 220 comboBox.setEditor(editor.wrapped); 221 222 //remove old key listener 223 for (KeyListener l : editorComponent.getKeyListeners()) { 224 if (l instanceof AutoComplete.KeyAdapter) { 225 editorComponent.removeKeyListener(l); 226 break; 227 } 228 } 229 230 undecorate(editorComponent); 231 232 for (ActionListener l : comboBox.getActionListeners()) { 233 if (l instanceof ComboBoxAdaptor) { 234 comboBox.removeActionListener(l); 235 break; 236 } 237 } 238 239 //TODO remove aqua fix 240 241 //TODO reset editibility 242 } 243 } 244 245 /** 246 * Enables automatic completion for the given JTextComponent based on the 247 * items contained in the given JList. The two components will be 248 * synchronized. The automatic completion will always be strict. 249 * @param list a <tt>JList</tt> containing the items for automatic completion 250 * @param textComponent the text component that will be enabled for automatic 251 * completion 252 */ 253 public static void decorate(JList list, JTextComponent textComponent) { 254 decorate(list, textComponent, null); 255 } 256 257 /** 258 * Enables automatic completion for the given JTextComponent based on the 259 * items contained in the given JList. The two components will be 260 * synchronized. The automatic completion will always be strict. 261 * @param list a <tt>JList</tt> containing the items for automatic completion 262 * @param textComponent the text component that will be used for automatic 263 * completion 264 * @param stringConverter the converter used to transform items to strings 265 */ 266 public static void decorate(JList list, JTextComponent textComponent, ObjectToStringConverter stringConverter) { 267 undecorate(list); 268 269 AbstractAutoCompleteAdaptor adaptor = new ListAdaptor(list, textComponent, stringConverter); 270 AutoCompleteDocument document = createAutoCompleteDocument(adaptor, true, stringConverter, textComponent.getDocument()); 271 decorate(textComponent, document, adaptor); 272 } 273 274 static void undecorate(JList list) { 275 for (ListSelectionListener l : list.getListSelectionListeners()) { 276 if (l instanceof ListAdaptor) { 277 list.removeListSelectionListener(l); 278 break; 279 } 280 } 281 } 282 283 /** 284 * Enables automatic completion for the given JTextComponent based on the 285 * items contained in the given <tt>List</tt>. 286 * @param textComponent the text component that will be used for automatic 287 * completion. 288 * @param items contains the items that are used for autocompletion 289 * @param strictMatching <tt>true</tt>, if only given items should be allowed to be entered 290 */ 291 public static void decorate(JTextComponent textComponent, List<?> items, boolean strictMatching) { 292 decorate(textComponent, items, strictMatching, null); 293 } 294 295 /** 296 * Enables automatic completion for the given JTextComponent based on the 297 * items contained in the given <tt>List</tt>. 298 * @param items contains the items that are used for autocompletion 299 * @param textComponent the text component that will be used for automatic 300 * completion. 301 * @param strictMatching <tt>true</tt>, if only given items should be allowed to be entered 302 * @param stringConverter the converter used to transform items to strings 303 */ 304 public static void decorate(JTextComponent textComponent, List<?> items, boolean strictMatching, ObjectToStringConverter stringConverter) { 305 AbstractAutoCompleteAdaptor adaptor = new TextComponentAdaptor(textComponent, items); 306 AutoCompleteDocument document = createAutoCompleteDocument(adaptor, strictMatching, stringConverter, textComponent.getDocument()); 307 decorate(textComponent, document, adaptor); 308 } 309 310 /** 311 * Decorates a given text component for automatic completion using the 312 * given AutoCompleteDocument and AbstractAutoCompleteAdaptor. 313 * 314 * @param textComponent a text component that should be decorated 315 * @param document the AutoCompleteDocument to be installed on the text component 316 * @param adaptor the AbstractAutoCompleteAdaptor to be used 317 */ 318 public static void decorate(JTextComponent textComponent, AutoCompleteDocument document, AbstractAutoCompleteAdaptor adaptor) { 319 undecorate(textComponent); 320 321 // install the document on the text component 322 textComponent.setDocument(document); 323 324 // mark entire text when the text component gains focus 325 // otherwise the last mark would have been retained which is quiet confusing 326 textComponent.addFocusListener(new AutoComplete.FocusAdapter(adaptor)); 327 328 // Tweak some key bindings 329 InputMap editorInputMap = textComponent.getInputMap(); 330 331 while (editorInputMap != null) { 332 InputMap parent = editorInputMap.getParent(); 333 334 if (parent instanceof UIResource) { 335 installMap(editorInputMap, document.isStrictMatching()); 336 break; 337 } 338 339 editorInputMap = parent; 340 } 341 342 ActionMap editorActionMap = textComponent.getActionMap(); 343 editorActionMap.put("nonstrict-backspace", new NonStrictBackspaceAction( 344 editorActionMap.get(DefaultEditorKit.deletePrevCharAction), 345 editorActionMap.get(DefaultEditorKit.selectionBackwardAction), 346 adaptor)); 347 } 348 349 static void undecorate(JTextComponent textComponent) { 350 Document doc = textComponent.getDocument(); 351 352 if (doc instanceof AutoCompleteDocument) { 353 //remove autocomplete key/action mappings 354 InputMap map = textComponent.getInputMap(); 355 356 while (map.getParent() != null) { 357 InputMap parent = map.getParent(); 358 359 if (parent instanceof AutoComplete.InputMap) { 360 map.setParent(parent.getParent()); 361 } 362 363 map = parent; 364 } 365 366 textComponent.getActionMap().put("nonstrict-backspace", null); 367 368 //remove old focus listener 369 for (FocusListener l : textComponent.getFocusListeners()) { 370 if (l instanceof AutoComplete.FocusAdapter) { 371 textComponent.removeFocusListener(l); 372 break; 373 } 374 } 375 376 //reset to original document 377 textComponent.setDocument(((AutoCompleteDocument) doc).delegate); 378 } 379 } 380 381 static class NonStrictBackspaceAction extends TextAction { 382 Action backspace; 383 Action selectionBackward; 384 AbstractAutoCompleteAdaptor adaptor; 385 386 public NonStrictBackspaceAction(Action backspace, Action selectionBackward, AbstractAutoCompleteAdaptor adaptor) { 387 super("nonstrict-backspace"); 388 this.backspace = backspace; 389 this.selectionBackward = selectionBackward; 390 this.adaptor = adaptor; 391 } 392 393 @Override 394 public void actionPerformed(ActionEvent e) { 395 if (adaptor.listContainsSelectedItem()) { 396 selectionBackward.actionPerformed(e); 397 } else { 398 backspace.actionPerformed(e); 399 } 400 } 401 } 402}