com.jidesoft.swing.AutoCompletion Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jide-oss Show documentation
Show all versions of jide-oss Show documentation
JIDE Common Layer (Professional Swing Components)
/*
* @(#)AutoCompletion.java 6/22/2005
*
* Copyright 2002 - 2005 JIDE Software Inc. All rights reserved.
*/
package com.jidesoft.swing;
import com.jidesoft.utils.PortingUtils;
import com.jidesoft.utils.SystemInfo;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.text.*;
import javax.swing.tree.TreePath;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
/**
* AutoCompletion
is a helper class to make JTextComponent or JComboBox auto-complete based on a list of
* known items.
*
* There are three constructors. The simplest one is {@link #AutoCompletion(javax.swing.JComboBox)}. It takes any
* combobox and make it auto completion. If you are looking for an auto-complete combobox solution, this is all you
* need. However AutoCompletion
can do more than that. There are two more constructors. One is {@link
* #AutoCompletion(javax.swing.text.JTextComponent, Searchable)}. It will use {@link Searchable} which is another
* component available in JIDE to make the JTextCompoent auto-complete. We used Searchable here because it provides a
* common interface to access the element in JTree, JList or JTable. In the other word, the known list item we used to
* auto-complete can be got from JTree or JList or even JTable or any other component as long as it has Searchable
* interface implemented. The last constructor takes any java.util.List and use it as auto completion list.
*
* The only option available on AutoCompletion
is {@link #setStrict(boolean)}. If it's true, it will not
* allow user to type in anything that is not in the known item list. If false, user can type in whatever he/she wants.
* If the text can match with a item in the known item list, it will still auto-complete.
*
*
* @author Thomas Bierhance
* @author JIDE Software, Inc.
*/
public class AutoCompletion {
private Searchable _searchable;
private JTextComponent _textComponent;
private AutoCompletionDocument _document;
// flag to indicate if setSelectedItem has been called
// subsequent calls to remove/insertString should be ignored
private boolean _selecting = false;
private boolean _hidePopupOnFocusLoss;
// we use this flag to workaround the bug that setText() will trigger auto-completion
private boolean _keyTyped = false;
private boolean _hitBackspace = false;
private boolean _hitBackspaceOnSelection;
private KeyListener _editorKeyListener;
// private FocusListener _editorFocusListener;
private boolean _strict = true;
private boolean _strictCompletion = true;
private PropertyChangeListener _propertyChangeListener;
private JComboBox _comboBox;
private Document _oldDocument;
/**
* The client property for AutoCompletion instance. When AutoCompletion is installed on a text component, this
* client property has the AutoCompletion.
*/
public static final String CLIENT_PROPERTY_AUTO_COMPLETION = "AutoCompletion";
public AutoCompletion(final JComboBox comboBox) {
this(comboBox, new ComboBoxSearchable(comboBox));
}
public AutoCompletion(final JComboBox comboBox, Searchable searchable) {
_searchable = searchable;
_propertyChangeListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent e) {
if ("editor".equals(e.getPropertyName())) {
if (e.getNewValue() != null) {
_textComponent = (JTextComponent) ((ComboBoxEditor) e.getNewValue()).getEditorComponent();
configureEditor(getTextComponent());
}
}
}
};
_comboBox = comboBox;
_searchable.setWildcardEnabled(false);
if (_searchable instanceof ComboBoxSearchable) {
((ComboBoxSearchable) _searchable).setShowPopupDuringSearching(false);
}
_textComponent = (JTextComponent) comboBox.getEditor().getEditorComponent();
installListeners();
}
public AutoCompletion(final JTextComponent textComponent, final Searchable searchable) {
_searchable = searchable;
_searchable.setWildcardEnabled(false);
_textComponent = textComponent;
registerSelectionListener(getSearchable());
installListeners();
}
public AutoCompletion(final JTextComponent textComponent, final List list) {
this(textComponent, new Searchable(new JLabel()) {
int _selectIndex = -1;
@Override
protected int getSelectedIndex() {
return _selectIndex;
}
@Override
protected void setSelectedIndex(int index, boolean incremental) {
_selectIndex = index;
}
@Override
protected int getElementCount() {
return list.size();
}
@Override
protected Object getElementAt(int index) {
return list.get(index);
}
@Override
protected String convertElementToString(Object element) {
return "" + element;
}
});
}
public AutoCompletion(final JTextComponent textComponent, final Object[] array) {
this(textComponent, new Searchable(new JLabel()) {
int _selectIndex = -1;
@Override
protected int getSelectedIndex() {
return _selectIndex;
}
@Override
protected void setSelectedIndex(int index, boolean incremental) {
_selectIndex = index;
}
@Override
protected int getElementCount() {
return array.length;
}
@Override
protected Object getElementAt(int index) {
return array[index];
}
@Override
protected String convertElementToString(Object element) {
return "" + element;
}
});
}
private void registerSelectionListener(Searchable searchable) {
if (searchable.getComponent() instanceof JList) {
final JList list = (JList) getSearchable().getComponent();
list.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
int index = list.getSelectedIndex();
if (index != -1) {
getTextComponent().setText(getSearchable().convertElementToString(list.getModel().getElementAt(index)));
highlightCompletedText(0);
}
}
});
DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, list, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0));
DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, list, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0));
DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, list, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0));
DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, list, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0));
}
else if (searchable.getComponent() instanceof JTree) {
final JTree tree = (JTree) getSearchable().getComponent();
tree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
public void valueChanged(TreeSelectionEvent e) {
TreePath treePath = tree.getSelectionPath();
if (treePath != null) {
getTextComponent().setText("" + treePath.getLastPathComponent());
highlightCompletedText(0);
}
else {
getTextComponent().setText("");
}
}
});
DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, tree, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0));
DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, tree, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0));
DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, tree, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0));
DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, tree, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0));
}
}
private boolean isKeyTyped() {
return _keyTyped;
}
private void setKeyTyped(boolean keyTyped) {
_keyTyped = keyTyped;
}
private void setInitValue() {
int index = getSearchable().getSelectedIndex();
if (index != -1) {
Object selected = getSearchable().getElementAt(index);
if (selected != null)
_document.setText(getSearchable().convertElementToString(selected));
highlightCompletedText(0);
}
else {
_document.setText("");
}
}
/**
* Uninstalls the listeners so that the component is not auto-completion anymore.
*/
public void uninstallListeners() {
if (_propertyChangeListener != null && _comboBox != null) {
_comboBox.removePropertyChangeListener(_propertyChangeListener);
}
if (getTextComponent() != null) {
getTextComponent().removeKeyListener(_editorKeyListener);
// getTextComponent().removeFocusListener(_editorFocusListener);
String text = getTextComponent().getText();
if (_oldDocument != null) {
getTextComponent().setDocument(_oldDocument);
_oldDocument = null;
}
getTextComponent().setText(text);
}
getTextComponent().putClientProperty(CLIENT_PROPERTY_AUTO_COMPLETION, null);
}
/**
* Installs the listeners needed for auto-completion feature. Please note, this method is already called when you
* create AutoCompletion. Unless you called {@link #uninstallListeners()}, there is no need to call this method
* yourself.
*/
public void installListeners() {
if (_comboBox != null && _propertyChangeListener != null) {
_comboBox.addPropertyChangeListener(_propertyChangeListener);
}
_editorKeyListener = new KeyAdapter() {
private boolean _deletePressed;
private String _saveText;
@Override
public void keyPressed(KeyEvent e) {
_hitBackspace = false;
if (KeyEvent.VK_ESCAPE != e.getKeyCode()) {
setKeyTyped(true);
}
switch (e.getKeyCode()) {
// determine if the pressed key is backspace (needed by the remove method)
case KeyEvent.VK_BACK_SPACE:
if (isStrict()) {
_hitBackspace = true;
_hitBackspaceOnSelection = getTextComponent().getSelectionStart() != getTextComponent().getSelectionEnd();
}
break;
// ignore delete key
case KeyEvent.VK_DELETE:
if (isStrict()) {
_deletePressed = true;
_saveText = getTextComponent().getText();
}
break;
}
}
@Override
public void keyReleased(KeyEvent e) {
super.keyReleased(e);
try {
if (_deletePressed) {
_deletePressed = false;
String text = getTextComponent().getText();
int index = getSearchable().findFirst(text);
if (index != -1) {
if (text.length() == 0) {
setSelectedItem("");
}
else {
Object item = getSearchable().getElementAt(index);
setSelectedItem(item);
getTextComponent().setText(getSearchable().convertElementToString(item)); // this is what auto complete is
// select the completed part
highlightCompletedText(text.length());
}
}
else { // didn't find a matching one
if (isStrict()) {
getTextComponent().setText(_saveText);
e.consume();
PortingUtils.notifyUser(_textComponent);
}
}
}
}
finally {
if (KeyEvent.VK_ESCAPE != e.getKeyCode()) {
setKeyTyped(false);
}
}
}
};
// Bug 5100422 on Java 1.5: Editable JComboBox won't hide popup when tabbing out
_hidePopupOnFocusLoss = SystemInfo.isJdk15Above();
// // Highlight whole text when gaining focus
// _editorFocusListener = new FocusAdapter() {
// @Override
// public void focusGained(FocusEvent e) {
// highlightCompletedText(0);
// }
//
// @Override
// public void focusLost(FocusEvent e) {
// // Workaround for Bug 5100422 - Hide Popup on focus loss
//// if (_hidePopupOnFocusLoss) comboBox.setPopupVisible(false);
// }
// };
_document = createDocument();
configureEditor(getTextComponent());
getTextComponent().putClientProperty(CLIENT_PROPERTY_AUTO_COMPLETION, this);
}
/**
* Creates AutoCompletionDocument.
*
* @return the AutoCompletionDocument.
*/
protected AutoCompletionDocument createDocument() {
return new AutoCompletionDocument();
}
private void configureEditor(JTextComponent textComponent) {
if (getTextComponent() != null) {
getTextComponent().removeKeyListener(_editorKeyListener);
// getTextComponent().removeFocusListener(_editorFocusListener);
}
if (textComponent != null) {
_textComponent = textComponent;
getTextComponent().addKeyListener(_editorKeyListener);
// getTextComponent().addFocusListener(_editorFocusListener);
String text = getTextComponent().getText();
_oldDocument = getTextComponent().getDocument();
if (_oldDocument instanceof AbstractDocument && _document != null) {
_document.setDocumentFilter(((AbstractDocument) _oldDocument).getDocumentFilter());
}
getTextComponent().setDocument(_document);
getTextComponent().setText(text);
}
}
/**
* The document class used by AutoCompletion.
*/
protected class AutoCompletionDocument extends PlainDocument {
@Override
public void remove(int offs, int len) throws BadLocationException {
// return immediately when _selecting an item
if (_selecting)
return;
if (_hitBackspace) {
// user hit backspace => move the selection backwards
// old item keeps being selected
if (offs > 0) {
if (_hitBackspaceOnSelection) offs--;
}
else {
// User hit backspace with the cursor positioned on the start => beep
PortingUtils.notifyUser(_textComponent);
}
highlightCompletedText(offs);
}
else {
super.remove(offs, len);
}
}
@Override
public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
// return immediately when _selecting an item
if (_selecting)
return;
// insert the string into the document
super.insertString(offs, str, a);
if (isKeyTyped() || isStrict()) {
// lookup and select a matching item
final String text = getText(0, getLength());
int exactIndex = getSearchable().findFirstExactly(text);
if (exactIndex != -1) {
return;
}
int index = getSearchable().findFromCursor(text);
Object item;
if (index != -1) {
item = getSearchable().getElementAt(index);
setSelectedItem(item);
setText(getSearchable().convertElementToString(item)); // this is what auto complete is
// select the completed part
highlightCompletedText(offs + str.length());
}
else { // didn't find a matching one
if (isStrict()) {
if (offs == 0 && text.equals(str)) {
getSearchable().textChanged("");
index = getSearchable().findFromCursor(text);
if (index != -1) {
item = getSearchable().getElementAt(index);
setSelectedItem(item);
setText(getSearchable().convertElementToString(item)); // this is what auto complete is
// select the completed part
highlightCompletedText(offs + str.length());
}
else { // didn't find a matching one
if (isStrict()) {
index = getSearchable().getSelectedIndex();
if (index == -1) {
if (getSearchable().getElementCount() > 0) {
index = 0;
getSearchable().setSelectedIndex(0, false);
}
}
if (index != -1) {
item = getSearchable().getElementAt(index);
offs = offs - str.length();
// imitate no insert (later on offs will be incremented by str.length(): selection won't move forward)
PortingUtils.notifyUser(_textComponent);
setText(getSearchable().convertElementToString(item));
// select the completed part
highlightCompletedText(offs + str.length());
}
}
}
return;
}
index = getSearchable().getSelectedIndex();
if (index == -1) {
if (getSearchable().getElementCount() > 0) {
index = 0;
getSearchable().setSelectedIndex(0, false);
}
}
if (index != -1) {
item = getSearchable().getElementAt(index);
offs = offs - str.length();
// imitate no insert (later on offs will be incremented by str.length(): selection won't move forward)
PortingUtils.notifyUser(_textComponent);
setText(getSearchable().convertElementToString(item));
// select the completed part
highlightCompletedText(offs + str.length());
}
}
}
}
}
protected void setText(String text) {
try {
// remove all text and insert the completed string
if (isStrictCompletion()) {
super.remove(0, getLength());
super.insertString(0, text, null);
}
else {
String existingText = super.getText(0, getLength());
int matchIndex = existingText.length() <= text.length() ? existingText.length() : text.length();
// try to find a match
for (int i = 0; i < existingText.length(); i++) {
if (!existingText.substring(0, matchIndex).equalsIgnoreCase(text.substring(0, matchIndex))) {
matchIndex--;
}
}
// remove the no-match part and complete with the one in Searchable
super.remove(matchIndex, getLength() - matchIndex);
super.insertString(matchIndex, text.substring(matchIndex), null);
}
}
catch (BadLocationException e) {
throw new RuntimeException(e.toString());
}
}
}
private void highlightCompletedText(int start) {
int length = getTextComponent().getDocument().getLength();
getTextComponent().setCaretPosition(length);
if (start < 0) {
start = 0;
}
if (start > length) {
start = length;
}
getTextComponent().moveCaretPosition(start);
}
private void setSelectedItem(Object item) {
_selecting = true;
for (int i = 0, n = getSearchable().getElementCount(); i < n; i++) {
Object currentItem = getSearchable().getElementAt(i);
// current item starts with the pattern?
if (JideSwingUtilities.equals(item, currentItem)) {
getSearchable().setSelectedIndex(i, false);
}
}
_selecting = false;
}
/**
* Gets the strict property.
*
* @return the value of strict property.
*/
public boolean isStrict() {
return _strict;
}
/**
* Sets the strict property. If true, it will not allow user to type in anything that is not in the known item list.
* If false, user can type in whatever he/she wants. If the text can match with a item in the known item list, it
* will still auto-complete.
*
* @param strict
*/
public void setStrict(boolean strict) {
_strict = strict;
}
/**
* Gets the strict completion property.
*
* @return the value of strict completion property.
* @see #setStrictCompletion(boolean)
*/
public boolean isStrictCompletion() {
return _strictCompletion;
}
/**
* Sets the strict completion property. If true, in case insensitive searching, it will always use the exact item in
* the Searchable to replace whatever user types. For example, when Searchable has an item "Arial" and user types in
* "AR", if this flag is true, it will auto-completed as "Arial". If false, it will be auto-completed as "ARial". Of
* course, this flag will only make a difference if Searchable is case insensitive.
*
* @param strictCompletion
*/
public void setStrictCompletion(boolean strictCompletion) {
_strictCompletion = strictCompletion;
}
/**
* Gets the underlying text component which auto-completes.
*
* @return the underlying text component.
*/
protected JTextComponent getTextComponent() {
return _textComponent;
}
/**
* Gets the underlying Searchable. If you use the constructor {@link #AutoCompletion(javax.swing.text.JTextComponent, Searchable)},
* the return value will be the Searchable you passed in. If you use the other twoconstructorss, internally we will
* still create a Searchable. If so, this Searchable will be returned.
*
* @return the Searchable.
*/
public Searchable getSearchable() {
return _searchable;
}
/**
* When auto-completion is enabled on a text component, we will set a client property on it. This method will look
* for this client property and return you the instance of the AutoCompletion that is installed on the component.
*
* @param component the component.
* @return the AutoCompletion. If null, it means there is no AutoCompletion installed.
*/
public static AutoCompletion getAutoCompletion(JComponent component) {
if (component == null) return null;
Object clientProperty = component.getClientProperty(CLIENT_PROPERTY_AUTO_COMPLETION);
if (clientProperty instanceof AutoCompletion) {
return (AutoCompletion) clientProperty;
}
return null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy