/* Jajuk Specific version of this swingx class to fix
* this: https://swingx.dev.java.net/issues/show_bug.cgi?id=464
*
* This file has been adapted to Jajuk by the Jajuk Team.
* Jajuk Copyright (C) 2007 The Jajuk Team
*
* The original copyrights and license follow:
*
* 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 ext;
import javax.swing.UIManager;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.PlainDocument;
import org.jdesktop.swingx.autocomplete.AbstractAutoCompleteAdaptor;
import org.jdesktop.swingx.autocomplete.ObjectToStringConverter;
/**
* 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.
*/
public class AutoCompleteDocument extends PlainDocument {
/** Generated serialVersionUID. */
private static final long serialVersionUID = -4353609211147483101L;
/** 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;
/** true, if only items from the adaptors's list can be entered false, otherwise (selected item might not be in the adaptors's list). */
boolean strictMatching;
/** The adaptor that is used to find and select items. */
AbstractAutoCompleteAdaptor adaptor;
ObjectToStringConverter stringConverter;
/**
* 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 = adaptor;
this.strictMatching = strictMatching;
this.stringConverter = stringConverter;
// Handle initially selected object
Object selected = adaptor.getSelectedItem();
if (selected != null) {
setText(stringConverter.getPreferredStringForItem(selected));
}
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
*/
public AutoCompleteDocument(AbstractAutoCompleteAdaptor adaptor, boolean strictMatching) {
this(adaptor, strictMatching, ObjectToStringConverter.DEFAULT_IMPLEMENTATION);
}
/**
* 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;
}
/* (non-Javadoc)
* @see javax.swing.text.AbstractDocument#remove(int, int)
*/
@Override
public void remove(int offs, int len) throws BadLocationException {
// return immediately when selecting an item
if (selecting) {
return;
}
super.remove(offs, len);
if (!strictMatching) {
setSelectedItem(getText(0, getLength()), getText(0, getLength()));
adaptor.getTextComponent().setCaretPosition(offs);
}
}
/* (non-Javadoc)
* @see javax.swing.text.PlainDocument#insertString(int, java.lang.String, javax.swing.text.AttributeSet)
*/
@Override
public void insertString(int pOffs, String str, AttributeSet a) throws BadLocationException {
int offs = pOffs;
// return immediately when selecting an item
if (selecting) {
return;
}
// insert the string into the document
super.insertString(offs, str, a);
// lookup and select a matching item
LookupResult lookupResult = lookupItem(getText(0, getLength()));
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 = offs - str.length();
// 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
adaptor.markText(offs + str.length());
}
/**
* 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
super.remove(0, getLength());
super.insertString(0, text, null);
} catch (BadLocationException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Selects the given item using the AbstractAutoCompleteAdaptor.
*
* @param item the item that is to be selected
* @param itemAsString string representation of the item 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 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 <code>null</code> if
* no item matches
*/
private LookupResult lookupItem(String pattern) {
// iterate over all items to find an exact match
LookupResult ret = findMatch(pattern, true);
if (ret != null) {
return ret;
}
// check if the currently selected item matches
Object selectedItem = adaptor.getSelectedItem();
String[] possibleStrings = stringConverter.getPossibleStringsForItem(selectedItem);
if (possibleStrings != null) {
for (String element : possibleStrings) {
if (startsWith(element, pattern)) {
return new LookupResult(selectedItem, element);
}
}
}
// search for any matching item, if the currently selected does not match
ret = findMatch(pattern, false);
if (ret != null) {
return ret;
}
// no item starts with the pattern => return null
return new LookupResult(null, "");
}
/**
* Find match.
*
* @param pattern
* @param exactMatch
*
* @return the lookup result
*/
private LookupResult findMatch(final String pattern, final boolean exactMatch) {
String[] possibleStrings;
for (int i = 0, n = adaptor.getItemCount(); i < n; i++) {
Object currentItem = adaptor.getItem(i);
possibleStrings = stringConverter.getPossibleStringsForItem(currentItem);
if (possibleStrings != null) {
// check if current item exactly matches the pattern
// or starts with the string depending on flag
for (String element : possibleStrings) {
if ((exactMatch && element.equals(pattern))
|| (!exactMatch && startsWith(element, pattern))) {
return new LookupResult(currentItem, element);
}
}
}
}
return null;
}
/**
* .
*/
private static class LookupResult {
Object matchingItem;
String matchingString;
/**
* Instantiates a new lookup result.
*
* @param matchingItem
* @param matchingString
*/
public LookupResult(Object matchingItem, String matchingString) {
this.matchingItem = matchingItem;
this.matchingString = matchingString;
}
}
/**
* Returns true if <code>base</code> starts with <code>prefix</code>
* (taking case into account).
*
* @param base the string to be checked
* @param prefix the prefix to check for
*
* @return true if <code>base</code> starts with <code>prefix</code>;
* false otherwise
*/
private boolean startsWith(String base, String prefix) {
if (base.length() < prefix.length()) {
return false;
}
return base.regionMatches(false, 0, prefix, 0, prefix.length());
}
}