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 }