org.jdesktop.swingx.autocomplete.AutoCompleteDocument Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of swingx-all Show documentation
Show all versions of swingx-all Show documentation
Fork of the inactive swingx-all library
/*
* $Id: AutoCompleteDocument.java 4051 2011-07-19 20:17:05Z 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 org.jdesktop.swingx.util.Contract;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.EventListenerList;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.PlainDocument;
import javax.swing.text.Position;
import javax.swing.text.Segment;
import java.util.Comparator;
import static org.jdesktop.swingx.autocomplete.ObjectToStringConverter.DEFAULT_IMPLEMENTATION;
/**
* A document that can be plugged into any JTextComponent to enable automatic completion.
* It finds and selects matching items using any implementation of the AbstractAutoCompleteAdaptor.
*/
@SuppressWarnings("nls")
public class AutoCompleteDocument implements Document {
private class Handler implements DocumentListener, UndoableEditListener {
private final EventListenerList listenerList = new EventListenerList();
public void addDocumentListener(DocumentListener listener) {
listenerList.add(DocumentListener.class, listener);
}
public void addUndoableEditListener(UndoableEditListener listener) {
listenerList.add(UndoableEditListener.class, listener);
}
/**
* {@inheritDoc}
*/
public void removeDocumentListener(DocumentListener listener) {
listenerList.remove(DocumentListener.class, listener);
}
/**
* {@inheritDoc}
*/
public void removeUndoableEditListener(UndoableEditListener listener) {
listenerList.remove(UndoableEditListener.class, listener);
}
/**
* {@inheritDoc}
*/
@Override
public void changedUpdate(DocumentEvent e) {
e = new DelegatingDocumentEvent(AutoCompleteDocument.this, e);
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == DocumentListener.class) {
((DocumentListener) listeners[i + 1]).changedUpdate(e);
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void insertUpdate(DocumentEvent e) {
e = new DelegatingDocumentEvent(AutoCompleteDocument.this, e);
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == DocumentListener.class) {
((DocumentListener) listeners[i + 1]).insertUpdate(e);
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void removeUpdate(DocumentEvent e) {
e = new DelegatingDocumentEvent(AutoCompleteDocument.this, e);
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == DocumentListener.class) {
((DocumentListener) listeners[i + 1]).removeUpdate(e);
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void undoableEditHappened(UndoableEditEvent e) {
e = new UndoableEditEvent(AutoCompleteDocument.this, e.getEdit());
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == UndoableEditListener.class) {
((UndoableEditListener) listeners[i + 1]).undoableEditHappened(e);
}
}
}
}
/**
* true, if only items from the adaptors's list can be entered
* false, otherwise (selected item might not be in the adaptors's list)
*/
protected boolean strictMatching;
protected final Document delegate;
/**
* Flag to indicate if adaptor.setSelectedItem has been called.
* Subsequent calls to remove/insertString should be ignored
* as they are likely have been caused by the adapted Component that
* is trying to set the text for the selected component.
*/
boolean selecting = false;
/**
* The adaptor that is used to find and select items.
*/
private final AbstractAutoCompleteAdaptor adaptor;
final ObjectToStringConverter stringConverter;
private final Handler handler;
// Note: these comparators do not impose any ordering - e.g. they do not ensure that sgn(compare(x, y)) == -sgn(compare(y, x))
private static final Comparator EQUALS_IGNORE_CASE = (o1, o2) -> o1.equalsIgnoreCase(o2) ? 0 : -1;
private static final Comparator STARTS_WITH_IGNORE_CASE = (o1, o2) -> {
if (o1.length() < o2.length())
return -1;
return o1.regionMatches(true, 0, o2, 0, o2.length()) ? 0 : -1;
};
private static final Comparator EQUALS = (o1, o2) -> o1.equals(o2) ? 0 : -1;
private static final Comparator STARTS_WITH = (o1, o2) -> o1.startsWith(o2) ? 0 : -1;
/**
* Creates a new AutoCompleteDocument for the given AbstractAutoCompleteAdaptor.
*
* @param adaptor The adaptor that will be used to find and select matching
* items.
* @param strictMatching true, if only items from the adaptor's list should
* be allowed to be entered
* @param stringConverter the converter used to transform items to strings
* @param delegate the {@code Document} delegate backing this document
*/
public AutoCompleteDocument(AbstractAutoCompleteAdaptor adaptor,
boolean strictMatching,
ObjectToStringConverter stringConverter,
Document delegate) {
this.adaptor = Contract.asNotNull(adaptor, "adaptor cannot be null");
this.strictMatching = strictMatching;
this.stringConverter = stringConverter == null ? DEFAULT_IMPLEMENTATION : stringConverter;
this.delegate = delegate == null ? createDefaultDocument() : delegate;
handler = new Handler();
this.delegate.addDocumentListener(handler);
// Handle initially selected object
Object selected = adaptor.getSelectedItem();
if (selected != null) {
String itemAsString = this.stringConverter.getPreferredStringForItem(selected);
setText(itemAsString);
adaptor.setSelectedItemAsString(itemAsString);
}
this.adaptor.markEntireText();
}
/**
* Creates a new AutoCompleteDocument for the given AbstractAutoCompleteAdaptor.
*
* @param adaptor The adaptor that will be used to find and select matching
* items.
* @param strictMatching true, if only items from the adaptor's list should
* be allowed to be entered
* @param stringConverter the converter used to transform items to strings
*/
public AutoCompleteDocument(AbstractAutoCompleteAdaptor adaptor, boolean strictMatching, ObjectToStringConverter stringConverter) {
this(adaptor, strictMatching, stringConverter, null);
}
/**
* Creates a new AutoCompleteDocument for the given AbstractAutoCompleteAdaptor.
*
* @param strictMatching true, if only items from the adaptor's list should
* be allowed to be entered
* @param adaptor The adaptor that will be used to find and select matching
* items.
*/
public AutoCompleteDocument(AbstractAutoCompleteAdaptor adaptor, boolean strictMatching) {
this(adaptor, strictMatching, null);
}
/**
* Creates the default backing document when no delegate is passed to this
* document.
*
* @return the default backing document
*/
protected Document createDefaultDocument() {
return new PlainDocument();
}
@Override
public void remove(int offs, int len) throws BadLocationException {
// return immediately when selecting an item
if (selecting)
return;
delegate.remove(offs, len);
if (!strictMatching) {
setSelectedItem(getText(0, getLength()), getText(0, getLength()));
adaptor.getTextComponent().setCaretPosition(offs);
}
}
@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
delegate.insertString(offs, str, a);
// lookup and select a matching item
LookupResult lookupResult;
String pattern = getText(0, getLength());
if (pattern == null || pattern.length() == 0) {
lookupResult = new LookupResult(null, "");
setSelectedItem(lookupResult.matchingItem, lookupResult.matchingString);
} else {
lookupResult = lookupItem(pattern);
}
if (lookupResult.matchingItem != null) {
setSelectedItem(lookupResult.matchingItem, lookupResult.matchingString);
} else {
if (strictMatching) {
// keep old item selected if there is no match
lookupResult.matchingItem = adaptor.getSelectedItem();
lookupResult.matchingString = adaptor.getSelectedItemAsString();
// imitate no insert (later on offs will be incremented by
// str.length(): selection won't move forward)
offs = str == null ? offs : offs - str.length();
if (str != null && !str.isEmpty()) {
// provide feedback to the user that his input has been received but can not be accepted
UIManager.getLookAndFeel().provideErrorFeedback(adaptor.getTextComponent());
}
} else {
// no item matches => use the current input as selected item
lookupResult.matchingItem = getText(0, getLength());
lookupResult.matchingString = getText(0, getLength());
setSelectedItem(lookupResult.matchingItem, lookupResult.matchingString);
}
}
setText(lookupResult.matchingString);
// select the completed part
int len = str == null ? 0 : str.length();
offs = lookupResult.matchingString == null ? 0 : offs + len;
adaptor.markText(offs);
}
/**
* Sets the text of this AutoCompleteDocument to the given text.
*
* @param text the text that will be set for this document
*/
private void setText(String text) {
try {
// remove all text and insert the completed string
delegate.remove(0, getLength());
delegate.insertString(0, text, null);
} catch (BadLocationException e) {
throw new RuntimeException(e.toString());
}
}
/**
* Selects the given item using the AbstractAutoCompleteAdaptor.
*
* @param itemAsString string representation of the item to be selected
* @param item the item that is to be selected
*/
private void setSelectedItem(Object item, String itemAsString) {
selecting = true;
adaptor.setSelectedItem(item);
adaptor.setSelectedItemAsString(itemAsString);
selecting = false;
}
/**
* Searches for an item that matches the given pattern. The AbstractAutoCompleteAdaptor
* is used to access the candidate items. The match is not case-sensitive
* and will only match at the beginning of each item's string representation.
*
* @param pattern the pattern that should be matched
* @return the first item that matches the pattern or null
if no item matches
*/
private LookupResult lookupItem(String pattern) {
Object selectedItem = adaptor.getSelectedItem();
LookupResult lookupResult;
// first try: case sensitive
lookupResult = lookupItem(pattern, EQUALS);
if (lookupResult != null)
return lookupResult;
lookupResult = lookupOneItem(selectedItem, pattern, STARTS_WITH);
if (lookupResult != null)
return lookupResult;
lookupResult = lookupItem(pattern, STARTS_WITH);
if (lookupResult != null)
return lookupResult;
// second try: ignore case
lookupResult = lookupItem(pattern, EQUALS_IGNORE_CASE);
if (lookupResult != null)
return lookupResult;
lookupResult = lookupOneItem(selectedItem, pattern, STARTS_WITH_IGNORE_CASE);
if (lookupResult != null)
return lookupResult;
lookupResult = lookupItem(pattern, STARTS_WITH_IGNORE_CASE);
if (lookupResult != null)
return lookupResult;
// no item starts with the pattern => return null
return new LookupResult(null, "");
}
private LookupResult lookupOneItem(Object item, String pattern, Comparator comparator) {
String[] possibleStrings = stringConverter.getPossibleStringsForItem(item);
if (possibleStrings != null) {
for (String possibleString : possibleStrings) {
if (comparator.compare(possibleString, pattern) == 0) {
return new LookupResult(item, possibleString);
}
}
}
return null;
}
private LookupResult lookupItem(String pattern, Comparator comparator) {
// iterate over all items and return first match
for (int i = 0, n = adaptor.getItemCount(); i < n; i++) {
Object currentItem = adaptor.getItem(i);
LookupResult result = lookupOneItem(currentItem, pattern, comparator);
if (result != null)
return result;
}
return null;
}
private static class LookupResult {
Object matchingItem;
String matchingString;
LookupResult(Object matchingItem, String matchingString) {
this.matchingItem = matchingItem;
this.matchingString = matchingString;
}
}
/**
* {@inheritDoc}
*/
@Override
public void addDocumentListener(DocumentListener listener) {
handler.addDocumentListener(listener);
}
/**
* {@inheritDoc}
*/
@Override
public void addUndoableEditListener(UndoableEditListener listener) {
handler.addUndoableEditListener(listener);
}
/**
* {@inheritDoc}
*/
@Override
public Position createPosition(int offs) throws BadLocationException {
return delegate.createPosition(offs);
}
/**
* {@inheritDoc}
*/
@Override
public Element getDefaultRootElement() {
return delegate.getDefaultRootElement();
}
/**
* {@inheritDoc}
*/
@Override
public Position getEndPosition() {
return delegate.getEndPosition();
}
/**
* {@inheritDoc}
*/
@Override
public int getLength() {
return delegate.getLength();
}
/**
* {@inheritDoc}
*/
@Override
public Object getProperty(Object key) {
return delegate.getProperty(key);
}
/**
* {@inheritDoc}
*/
@Override
public Element[] getRootElements() {
return delegate.getRootElements();
}
/**
* {@inheritDoc}
*/
@Override
public Position getStartPosition() {
return delegate.getStartPosition();
}
/**
* {@inheritDoc}
*/
@Override
public String getText(int offset, int length) throws BadLocationException {
return delegate.getText(offset, length);
}
/**
* {@inheritDoc}
*/
@Override
public void getText(int offset, int length, Segment txt) throws BadLocationException {
delegate.getText(offset, length, txt);
}
/**
* {@inheritDoc}
*/
@Override
public void putProperty(Object key, Object value) {
delegate.putProperty(key, value);
}
/**
* {@inheritDoc}
*/
@Override
public void removeDocumentListener(DocumentListener listener) {
handler.removeDocumentListener(listener);
}
/**
* {@inheritDoc}
*/
@Override
public void removeUndoableEditListener(UndoableEditListener listener) {
handler.removeUndoableEditListener(listener);
}
/**
* {@inheritDoc}
*/
@Override
public void render(Runnable r) {
delegate.render(r);
}
/**
* Returns if only items from the adaptor's list should be allowed to be entered.
*
* @return if only items from the adaptor's list should be allowed to be entered
*/
public boolean isStrictMatching() {
return strictMatching;
}
}