001 /* 002 * $Id: AutoCompleteDecorator.java,v 1.3 2006/04/30 16:38:47 Bierhance Exp $ 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 */ 021 package org.jdesktop.swingx.autocomplete; 022 023 import java.awt.event.ActionEvent; 024 import java.awt.event.FocusAdapter; 025 import java.awt.event.FocusEvent; 026 import java.awt.event.ItemEvent; 027 import java.awt.event.ItemListener; 028 import java.awt.event.KeyAdapter; 029 import java.awt.event.KeyEvent; 030 import java.awt.event.KeyListener; 031 import java.beans.PropertyChangeEvent; 032 import java.beans.PropertyChangeListener; 033 034 import javax.swing.Action; 035 import javax.swing.ActionMap; 036 import javax.swing.ComboBoxEditor; 037 import javax.swing.InputMap; 038 import javax.swing.JComboBox; 039 import javax.swing.JList; 040 import javax.swing.KeyStroke; 041 import javax.swing.UIManager; 042 import javax.swing.text.DefaultEditorKit; 043 import javax.swing.text.JTextComponent; 044 import javax.swing.text.TextAction; 045 046 /** 047 * This class contains only static utility methods that can be used to set up 048 * automatic completion for some Swing components. 049 * <p>Usage examples:</p> 050 * <p><code> 051 * JComboBox comboBox = [...];<br/> 052 * AutoCompleteDecorator.<b>decorate</b>(comboBox);<br/> 053 * <br/> 054 * JList list = [...];<br/> 055 * JTextField textField = [...];<br/> 056 * AutoCompleteDecorator.<b>decorate</b>(list, textField); 057 * </p></code> 058 * 059 * @author Thomas Bierhance 060 */ 061 public class AutoCompleteDecorator { 062 063 /** 064 * Enables automatic completion for the given JTextComponent based on the 065 * items contained in the given JList. The two components will be 066 * synchronized. The automatic completion will always be strict. 067 * 068 * @param list a list 069 * @param textComponent the text component that will be used for automatic 070 * completion. 071 */ 072 public static void decorate(JList list, JTextComponent textComponent) { 073 AbstractAutoCompleteAdaptor adaptor = new ListAdaptor(list, textComponent); 074 AutoCompleteDocument document = new AutoCompleteDocument(adaptor, true); 075 decorate(textComponent, document, adaptor); 076 } 077 078 /** 079 * Enables automatic completion for the given JComboBox. The automatic 080 * completion will be strict (only items from the combo box can be selected) 081 * if the combo box is not editable. 082 * @param comboBox a combobox 083 */ 084 public static void decorate(final JComboBox comboBox) { 085 boolean strictMatching = !comboBox.isEditable(); 086 // has to be editable 087 comboBox.setEditable(true); 088 089 // configure the text component=editor component 090 JTextComponent editor = (JTextComponent) comboBox.getEditor().getEditorComponent(); 091 final AbstractAutoCompleteAdaptor adaptor = new ComboBoxAdaptor(comboBox); 092 final AutoCompleteDocument document = new AutoCompleteDocument(adaptor, strictMatching); 093 decorate(editor, document, adaptor); 094 095 // show the popup list when the user presses a key 096 final KeyListener keyListener = new KeyAdapter() { 097 public void keyPressed(KeyEvent keyEvent) { 098 // don't popup on action keys (cursor movements, etc...) 099 if (keyEvent.isActionKey()) return; 100 // don't popup if the combobox isn't visible anyway 101 if (comboBox.isDisplayable() && !comboBox.isPopupVisible()) { 102 int keyCode = keyEvent.getKeyCode(); 103 // don't popup when the user hits shift,ctrl or alt 104 if (keyCode==keyEvent.VK_SHIFT || keyCode==keyEvent.VK_CONTROL || keyCode==keyEvent.VK_ALT) return; 105 // don't popup when the user hits escape (see issue #311) 106 if (keyCode==keyEvent.VK_ESCAPE) return; 107 comboBox.setPopupVisible(true); 108 } 109 } 110 }; 111 editor.addKeyListener(keyListener); 112 113 // Changing the l&f can change the combobox' editor which in turn 114 // would not be autocompletion-enabled. The new editor needs to be set-up. 115 comboBox.addPropertyChangeListener(new PropertyChangeListener() { 116 public void propertyChange(PropertyChangeEvent e) { 117 if (e.getPropertyName().equals("editor")) { 118 ComboBoxEditor editor = comboBox.getEditor(); 119 if (editor!=null && editor.getEditorComponent()!=null) { 120 decorate((JTextComponent) editor.getEditorComponent(), document, adaptor); 121 editor.getEditorComponent().addKeyListener(keyListener); 122 } 123 } 124 } 125 }); 126 } 127 128 /** 129 * Decorates a given text component for automatic completion using the 130 * given AutoCompleteDocument and AbstractAutoCompleteAdaptor. 131 * 132 * 133 * @param textComponent a text component that should be decorated 134 * @param document the AutoCompleteDocument to be installed on the text component 135 * @param adaptor the AbstractAutoCompleteAdaptor to be used 136 */ 137 public static void decorate(JTextComponent textComponent, AutoCompleteDocument document, final AbstractAutoCompleteAdaptor adaptor) { 138 // install the document on the text component 139 textComponent.setDocument(document); 140 141 // mark entire text when the text component gains focus 142 // otherwise the last mark would have been retained which is quiet confusing 143 textComponent.addFocusListener(new FocusAdapter() { 144 public void focusGained(FocusEvent e) { 145 JTextComponent textComponent = (JTextComponent) e.getSource(); 146 adaptor.markEntireText(); 147 } 148 }); 149 150 // Tweak some key bindings 151 InputMap editorInputMap = textComponent.getInputMap(); 152 if (document.isStrictMatching()) { 153 // move the selection to the left on VK_BACK_SPACE 154 editorInputMap.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_BACK_SPACE, 0), DefaultEditorKit.selectionBackwardAction); 155 // ignore VK_DELETE and CTRL+VK_X and beep instead when strict matching 156 editorInputMap.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_DELETE, 0), errorFeedbackAction); 157 editorInputMap.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_X, java.awt.event.InputEvent.CTRL_DOWN_MASK), errorFeedbackAction); 158 } else { 159 ActionMap editorActionMap = textComponent.getActionMap(); 160 // leave VK_DELETE and CTRL+VK_X as is 161 // VK_BACKSPACE will move the selection to the left if the selected item is in the list 162 // it will delete the previous character otherwise 163 editorInputMap.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_BACK_SPACE, 0), "nonstrict-backspace"); 164 editorActionMap.put("nonstrict-backspace", new NonStrictBackspaceAction( 165 editorActionMap.get(DefaultEditorKit.deletePrevCharAction), 166 editorActionMap.get(DefaultEditorKit.selectionBackwardAction), 167 adaptor)); 168 } 169 } 170 171 static class NonStrictBackspaceAction extends TextAction { 172 Action backspace; 173 Action selectionBackward; 174 AbstractAutoCompleteAdaptor adaptor; 175 176 public NonStrictBackspaceAction(Action backspace, Action selectionBackward, AbstractAutoCompleteAdaptor adaptor) { 177 super("nonstrict-backspace"); 178 this.backspace = backspace; 179 this.selectionBackward = selectionBackward; 180 this.adaptor = adaptor; 181 } 182 183 public void actionPerformed(ActionEvent e) { 184 if (adaptor.listContainsSelectedItem()) { 185 selectionBackward.actionPerformed(e); 186 } else { 187 backspace.actionPerformed(e); 188 } 189 } 190 } 191 192 /** 193 * A TextAction that provides an error feedback for the text component that invoked 194 * the action. The error feedback is most likely a "beep". 195 */ 196 static Object errorFeedbackAction = new TextAction("provide-error-feedback") { 197 public void actionPerformed(ActionEvent e) { 198 UIManager.getLookAndFeel().provideErrorFeedback(getTextComponent(e)); 199 } 200 }; 201 }