org.jdesktop.swingx.autocomplete.AutoCompleteDecorator Maven / Gradle / Ivy
/*
* $Id: AutoCompleteDecorator.java 3750 2010-08-08 04:01:13Z kschaefe $
*
* 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 static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusListener;
import java.awt.event.KeyListener;
import java.beans.PropertyChangeListener;
import java.util.List;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComboBox;
import javax.swing.JList;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.event.ListSelectionListener;
import javax.swing.plaf.UIResource;
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.
* Usage examples:
*
* JComboBox comboBox = [...];
* AutoCompleteDecorator.decorate(comboBox);
*
* List items = [...];
* JTextField textField = [...];
* AutoCompleteDecorator.decorate(textField, items);
*
* JList list = [...];
* JTextField textField = [...];
* AutoCompleteDecorator.decorate(list, textField);
*
*
* @author Thomas Bierhance
* @author Karl Schaefer
*/
public class AutoCompleteDecorator {
//these keys were pulled from BasicComboBoxUI from Sun JDK 1.6.0_20
private static final List COMBO_BOX_ACTIONS = unmodifiableList(asList("selectNext",
"selectNext2", "selectPrevious", "selectPrevious2", "pageDownPassThrough",
"pageUpPassThrough", "homePassThrough", "endPassThrough"));
/**
* A TextAction that provides an error feedback for the text component that invoked
* the action. The error feedback is most likely a "beep".
*/
private static final Object errorFeedbackAction = new TextAction("provide-error-feedback") {
public void actionPerformed(ActionEvent e) {
UIManager.getLookAndFeel().provideErrorFeedback(getTextComponent(e));
}
};
private AutoCompleteDecorator() {
//prevents instantiation
}
private static void installMap(InputMap componentMap, boolean strict) {
InputMap map = new AutoComplete.InputMap();
if (strict) {
map.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
map.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_DELETE, 0), errorFeedbackAction);
map.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_X, java.awt.event.InputEvent.CTRL_DOWN_MASK), errorFeedbackAction);
} else {
// VK_BACKSPACE will move the selection to the left if the selected item is in the list
// it will delete the previous character otherwise
map.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_BACK_SPACE, 0), "nonstrict-backspace");
// leave VK_DELETE and CTRL+VK_X as is
}
map.setParent(componentMap.getParent());
componentMap.setParent(map);
}
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 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(JComboBox comboBox) {
decorate(comboBox, null);
}
/**
* 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.
*
* Note: 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 accessed prior to being decorated and then the cached size is
* forced to be recalculated, the size of the component will change.
*
* 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(JComboBox comboBox, ObjectToStringConverter stringConverter) {
undecorate(comboBox);
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);
editorComponent.addKeyListener(new AutoComplete.KeyAdapter(comboBox));
//set before adding the listener for the editor
comboBox.setEditor(new AutoCompleteComboBoxEditor(comboBox.getEditor(), document.stringConverter));
// 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.
AutoComplete.PropertyChangeListener pcl = new AutoComplete.PropertyChangeListener(comboBox);
comboBox.addPropertyChangeListener("editor", pcl);
comboBox.addPropertyChangeListener("enabled", pcl);
if (!strictMatching) {
ActionMap map = comboBox.getActionMap();
for (String key : COMBO_BOX_ACTIONS) {
Action a = map.get(key);
map.put(key, new AutoComplete.SelectionAction(a));
}
}
}
static void undecorate(JComboBox comboBox) {
JTextComponent editorComponent = (JTextComponent) comboBox.getEditor().getEditorComponent();
if (editorComponent.getDocument() instanceof AutoCompleteDocument) {
AutoCompleteDocument doc = (AutoCompleteDocument) editorComponent.getDocument();
if (doc.strictMatching) {
ActionMap map = comboBox.getActionMap();
for (String key : COMBO_BOX_ACTIONS) {
map.put(key, null);
}
}
//remove old property change listener
for (PropertyChangeListener l : comboBox.getPropertyChangeListeners("editor")) {
if (l instanceof AutoComplete.PropertyChangeListener) {
comboBox.removePropertyChangeListener("editor", l);
}
}
for (PropertyChangeListener l : comboBox.getPropertyChangeListeners("enabled")) {
if (l instanceof AutoComplete.PropertyChangeListener) {
comboBox.removePropertyChangeListener("enabled", l);
}
}
AutoCompleteComboBoxEditor editor = (AutoCompleteComboBoxEditor) comboBox.getEditor();
comboBox.setEditor(editor.wrapped);
//remove old key listener
for (KeyListener l : editorComponent.getKeyListeners()) {
if (l instanceof AutoComplete.KeyAdapter) {
editorComponent.removeKeyListener(l);
break;
}
}
undecorate(editorComponent);
for (ActionListener l : comboBox.getActionListeners()) {
if (l instanceof ComboBoxAdaptor) {
comboBox.removeActionListener(l);
break;
}
}
//TODO remove aqua fix
//TODO reset editibility
}
}
/**
* 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 JList 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, null);
}
/**
* 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 JList 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) {
undecorate(list);
AbstractAutoCompleteAdaptor adaptor = new ListAdaptor(list, textComponent, stringConverter);
AutoCompleteDocument document = createAutoCompleteDocument(adaptor, true, stringConverter, textComponent.getDocument());
decorate(textComponent, document, adaptor);
}
static void undecorate(JList list) {
for (ListSelectionListener l : list.getListSelectionListeners()) {
if (l instanceof ListAdaptor) {
list.removeListSelectionListener(l);
break;
}
}
}
/**
* Enables automatic completion for the given JTextComponent based on the
* items contained in the given List.
* @param textComponent the text component that will be used for automatic
* completion.
* @param items contains the items that are used for autocompletion
* @param strictMatching true, if only given items should be allowed to be entered
*/
public static void decorate(JTextComponent textComponent, List items, boolean strictMatching) {
decorate(textComponent, items, strictMatching, null);
}
/**
* Enables automatic completion for the given JTextComponent based on the
* items contained in the given List.
* @param items contains the items that are used for autocompletion
* @param textComponent the text component that will be used for automatic
* completion.
* @param strictMatching true, 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);
}
/**
* 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, AbstractAutoCompleteAdaptor adaptor) {
undecorate(textComponent);
// install the document on the text component
textComponent.setDocument(document);
// mark entire text when the text component gains focus
// otherwise the last mark would have been retained which is quiet confusing
textComponent.addFocusListener(new AutoComplete.FocusAdapter(adaptor));
// Tweak some key bindings
InputMap editorInputMap = textComponent.getInputMap();
while (editorInputMap != null) {
InputMap parent = editorInputMap.getParent();
if (parent instanceof UIResource) {
installMap(editorInputMap, document.isStrictMatching());
break;
}
editorInputMap = parent;
}
ActionMap editorActionMap = textComponent.getActionMap();
editorActionMap.put("nonstrict-backspace", new NonStrictBackspaceAction(
editorActionMap.get(DefaultEditorKit.deletePrevCharAction),
editorActionMap.get(DefaultEditorKit.selectionBackwardAction),
adaptor));
}
static void undecorate(JTextComponent textComponent) {
Document doc = textComponent.getDocument();
if (doc instanceof AutoCompleteDocument) {
//remove autocomplete key/action mappings
InputMap map = textComponent.getInputMap();
while (map.getParent() != null) {
InputMap parent = map.getParent();
if (parent instanceof AutoComplete.InputMap) {
map.setParent(parent.getParent());
}
map = parent;
}
textComponent.getActionMap().put("nonstrict-backspace", null);
//remove old focus listener
for (FocusListener l : textComponent.getFocusListeners()) {
if (l instanceof AutoComplete.FocusAdapter) {
textComponent.removeFocusListener(l);
break;
}
}
//reset to original document
textComponent.setDocument(((AutoCompleteDocument) doc).delegate);
}
}
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);
}
}
}
}