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     * &nbsp;<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    }