/*
* $Id: AutoCompleteDecorator.java,v 1.18 2008/12/24 04:21:06 kschaefe Exp $
*
* Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jdesktop.swingx.autocomplete;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.ComboBoxEditor;
import javax.swing.InputMap;
import javax.swing.JComboBox;
import javax.swing.JList;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.StyledDocument;
import javax.swing.text.TextAction;
import org.jdesktop.swingx.autocomplete.workarounds.MacOSXPopupLocationFix;
/**
* This class contains only static utility methods that can be used to set up
* automatic completion for some Swing components.
* <p>Usage examples:</p>
* <p><pre><code>
* JComboBox comboBox = [...];
* AutoCompleteDecorator.<b>decorate</b>(comboBox);
*
* List items = [...];
* JTextField textField = [...];
* AutoCompleteDecorator.<b>decorate</b>(textField, items);
*
* JList list = [...];
* JTextField textField = [...];
* AutoCompleteDecorator.<b>decorate</b>(list, textField);
* </code></pre></p>
*
* @author Thomas Bierhance
*/
public class AutoCompleteDecorator {
private static void removeFocusListener(Component c) {
FocusListener[] listeners = c.getFocusListeners();
for (FocusListener l : listeners) {
if (l instanceof AutoCompleteFocusAdapter) {
c.removeFocusListener(l);
}
}
}
private static void removeKeyListener(Component c) {
KeyListener[] listeners = c.getKeyListeners();
for (KeyListener l : listeners) {
if (l instanceof AutoCompleteKeyAdapter) {
c.removeKeyListener(l);
}
}
}
private static void removePropertyChangeListener(Component c) {
PropertyChangeListener[] listeners = c.getPropertyChangeListeners("editor");
for (PropertyChangeListener l : listeners) {
if (l instanceof AutoCompletePropertyChangeListener) {
c.removePropertyChangeListener("editor", l);
}
}
}
private static AutoCompleteDocument createAutoCompleteDocument(
AbstractAutoCompleteAdaptor adaptor, boolean strictMatching,
ObjectToStringConverter stringConverter, Document delegate) {
if (delegate instanceof StyledDocument) {
return new AutoCompleteStyledDocument(adaptor, strictMatching,
stringConverter, (StyledDocument) delegate);
}
return new AutoCompleteDocument(adaptor, strictMatching,
stringConverter, delegate);
}
/**
* Enables automatic completion for the given JTextComponent based on the
* items contained in the given <tt>List</tt>.
* @param textComponent the text component that will be used for automatic
* completion.
* @param items contains the items that are used for autocompletion
* @param strictMatching <tt>true</tt>, if only given items should be allowed to be entered
*/
public static void decorate(JTextComponent textComponent, List<?> items, boolean strictMatching) {
decorate(textComponent, items, strictMatching, ObjectToStringConverter.DEFAULT_IMPLEMENTATION);
}
/**
* Enables automatic completion for the given JTextComponent based on the
* items contained in the given <tt>List</tt>.
* @param items contains the items that are used for autocompletion
* @param textComponent the text component that will be used for automatic
* completion.
* @param strictMatching <tt>true</tt>, if only given items should be allowed to be entered
* @param stringConverter the converter used to transform items to strings
*/
public static void decorate(JTextComponent textComponent, List<?> items, boolean strictMatching, ObjectToStringConverter stringConverter) {
AbstractAutoCompleteAdaptor adaptor = new TextComponentAdaptor(textComponent, items);
AutoCompleteDocument document = createAutoCompleteDocument(adaptor, strictMatching, stringConverter, textComponent.getDocument());
decorate(textComponent, document, adaptor);
}
/**
* Enables automatic completion for the given JTextComponent based on the
* items contained in the given JList. The two components will be
* synchronized. The automatic completion will always be strict.
* @param list a <tt>JList</tt> containing the items for automatic completion
* @param textComponent the text component that will be enabled for automatic
* completion
*/
public static void decorate(JList list, JTextComponent textComponent) {
decorate(list, textComponent, ObjectToStringConverter.DEFAULT_IMPLEMENTATION);
}
/**
* Enables automatic completion for the given JTextComponent based on the
* items contained in the given JList. The two components will be
* synchronized. The automatic completion will always be strict.
* @param list a <tt>JList</tt> containing the items for automatic completion
* @param textComponent the text component that will be used for automatic
* completion
* @param stringConverter the converter used to transform items to strings
*/
public static void decorate(JList list, JTextComponent textComponent, ObjectToStringConverter stringConverter) {
AbstractAutoCompleteAdaptor adaptor = new ListAdaptor(list, textComponent, stringConverter);
AutoCompleteDocument document = createAutoCompleteDocument(adaptor, true, stringConverter, textComponent.getDocument());
decorate(textComponent, document, adaptor);
}
/**
* Enables automatic completion for the given JComboBox. The automatic
* completion will be strict (only items from the combo box can be selected)
* if the combo box is not editable.
* @param comboBox a combo box
* @see #decorate(JComboBox, ObjectToStringConverter)
*/
public static void decorate(final JComboBox comboBox) {
decorate(comboBox, ObjectToStringConverter.DEFAULT_IMPLEMENTATION);
}
/**
* Enables automatic completion for the given JComboBox. The automatic
* completion will be strict (only items from the combo box can be selected)
* if the combo box is not editable.
* <p>
* <b>Note:</b> the {@code AutoCompleteDecorator} will alter the state of
* the {@code JComboBox} to be editable. This can cause side effects with
* layouts and sizing. {@code JComboBox} caches the size, which differs
* depending on the component's editability. Therefore, if the component's
* size is accesed prior to being decorated and then the cached size is
* forced to be recalculated, the size of the component will change.
* <p>
* Because the size of the component can be altered (recalculated), the
* decorator does not attempt to set any sizes on the supplied
* {@code JComboBox}. Users that need to ensure sizes of supplied combos
* should take measures to set the size of the combo.
*
* @param comboBox
* a combo box
* @param stringConverter
* the converter used to transform items to strings
*/
public static void decorate(final JComboBox comboBox, final ObjectToStringConverter stringConverter) {
boolean strictMatching = !comboBox.isEditable();
// has to be editable
comboBox.setEditable(true);
// fix the popup location
MacOSXPopupLocationFix.install(comboBox);
// configure the text component=editor component
JTextComponent editorComponent = (JTextComponent) comboBox.getEditor().getEditorComponent();
final AbstractAutoCompleteAdaptor adaptor = new ComboBoxAdaptor(comboBox);
final AutoCompleteDocument document = createAutoCompleteDocument(adaptor, strictMatching,
stringConverter, editorComponent.getDocument());
decorate(editorComponent, document, adaptor);
//remove old key listener
removeKeyListener(editorComponent);
// show the popup list when the user presses a key
final KeyListener keyListener = new AutoCompleteKeyAdapter() {
@Override
public void keyPressed(KeyEvent keyEvent) {
// don't popup on action keys (cursor movements, etc...)
if (keyEvent.isActionKey()) return;
// don't popup if the combobox isn't visible anyway
if (comboBox.isDisplayable() && !comboBox.isPopupVisible()) {
int keyCode = keyEvent.getKeyCode();
// don't popup when the user hits shift,ctrl or alt
if (keyCode==KeyEvent.VK_SHIFT || keyCode==KeyEvent.VK_CONTROL || keyCode==KeyEvent.VK_ALT) return;
// don't popup when the user hits escape (see issue #311)
if (keyCode==KeyEvent.VK_ENTER || keyCode==KeyEvent.VK_ESCAPE) return;
comboBox.setPopupVisible(true);
}
}
};
editorComponent.addKeyListener(keyListener);
if (stringConverter!=ObjectToStringConverter.DEFAULT_IMPLEMENTATION) {
comboBox.setEditor(new AutoCompleteComboBoxEditor(comboBox.getEditor(), stringConverter));
}
//remove old property change listener
removePropertyChangeListener(comboBox);
// Changing the l&f can change the combobox' editor which in turn
// would not be autocompletion-enabled. The new editor needs to be set-up.
comboBox.addPropertyChangeListener("editor", new AutoCompletePropertyChangeListener() {
public void propertyChange(PropertyChangeEvent e) {
ComboBoxEditor editor = (ComboBoxEditor) e.getOldValue();
if (editor != null && editor.getEditorComponent() != null) {
removeKeyListener(editor.getEditorComponent());
}
editor = (ComboBoxEditor) e.getNewValue();
if (editor!=null && editor.getEditorComponent()!=null) {
if (!(editor instanceof AutoCompleteComboBoxEditor)
&& stringConverter!=ObjectToStringConverter.DEFAULT_IMPLEMENTATION) {
comboBox.setEditor(new AutoCompleteComboBoxEditor(editor, stringConverter));
// Don't do the decorate step here because calling setEditor will trigger
// the propertychange listener a second time, which will do the decorate
// and addKeyListener step.
} else {
decorate((JTextComponent) editor.getEditorComponent(), document, adaptor);
editor.getEditorComponent().addKeyListener(keyListener);
}
}
}
});
}
/**
* Decorates a given text component for automatic completion using the
* given AutoCompleteDocument and AbstractAutoCompleteAdaptor.
*
*
* @param textComponent a text component that should be decorated
* @param document the AutoCompleteDocument to be installed on the text component
* @param adaptor the AbstractAutoCompleteAdaptor to be used
*/
public static void decorate(JTextComponent textComponent, AutoCompleteDocument document, final AbstractAutoCompleteAdaptor adaptor) {
// install the document on the text component
textComponent.setDocument(document);
//remove old focus listener
removeFocusListener(textComponent);
// mark entire text when the text component gains focus
// otherwise the last mark would have been retained which is quiet confusing
textComponent.addFocusListener(new AutoCompleteFocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
adaptor.markEntireText();
}
});
// Tweak some key bindings
InputMap editorInputMap = textComponent.getInputMap();
if (document.isStrictMatching()) {
// move the selection to the left on VK_BACK_SPACE
editorInputMap.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_BACK_SPACE, 0), DefaultEditorKit.selectionBackwardAction);
// ignore VK_DELETE and CTRL+VK_X and beep instead when strict matching
editorInputMap.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_DELETE, 0), errorFeedbackAction);
editorInputMap.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_X, java.awt.event.InputEvent.CTRL_DOWN_MASK), errorFeedbackAction);
} else {
ActionMap editorActionMap = textComponent.getActionMap();
// leave VK_DELETE and CTRL+VK_X as is
// VK_BACKSPACE will move the selection to the left if the selected item is in the list
// it will delete the previous character otherwise
editorInputMap.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_BACK_SPACE, 0), "nonstrict-backspace");
editorActionMap.put("nonstrict-backspace", new NonStrictBackspaceAction(
editorActionMap.get(DefaultEditorKit.deletePrevCharAction),
editorActionMap.get(DefaultEditorKit.selectionBackwardAction),
adaptor));
}
}
static class NonStrictBackspaceAction extends TextAction {
Action backspace;
Action selectionBackward;
AbstractAutoCompleteAdaptor adaptor;
public NonStrictBackspaceAction(Action backspace, Action selectionBackward, AbstractAutoCompleteAdaptor adaptor) {
super("nonstrict-backspace");
this.backspace = backspace;
this.selectionBackward = selectionBackward;
this.adaptor = adaptor;
}
public void actionPerformed(ActionEvent e) {
if (adaptor.listContainsSelectedItem()) {
selectionBackward.actionPerformed(e);
} else {
backspace.actionPerformed(e);
}
}
}
/**
* A TextAction that provides an error feedback for the text component that invoked
* the action. The error feedback is most likely a "beep".
*/
static Object errorFeedbackAction = new TextAction("provide-error-feedback") {
public void actionPerformed(ActionEvent e) {
UIManager.getLookAndFeel().provideErrorFeedback(getTextComponent(e));
}
};
}