001 /*
002 * $Id: AutoCompleteDocument.java,v 1.2 2006/04/06 18:51:34 Bierhance Exp $
003 *
004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
020 */
021 package org.jdesktop.swingx.autocomplete;
022
023 import javax.swing.UIManager;
024 import javax.swing.text.AttributeSet;
025 import javax.swing.text.BadLocationException;
026 import javax.swing.text.PlainDocument;
027
028 /**
029 * A document that can be plugged into any JTextComponent to enable automatic completion.
030 * It finds and selects matching items using any implementation of the AbstractAutoCompleteAdaptor.
031 */
032 public class AutoCompleteDocument extends PlainDocument {
033
034 /** Flag to indicate if adaptor.setSelectedItem has been called.
035 * Subsequent calls to remove/insertString should be ignored
036 * as they are likely have been caused by the adapted Component that
037 * is trying to set the text for the selected component.*/
038 boolean selecting=false;
039
040 /**
041 * true, if only items from the adaptors's list can be entered
042 * false, otherwise (selected item might not be in the adaptors's list)
043 */
044 boolean strictMatching;
045
046 /**
047 * The adaptor that is used to find and select items.
048 */
049 AbstractAutoCompleteAdaptor adaptor;
050
051 /**
052 * Creates a new AutoCompleteDocument for the given AbstractAutoCompleteAdaptor.
053 *
054 *
055 * @param strictMatching true, if only items from the adaptor's list should
056 * be allowed to be entered
057 * @param adaptor The adaptor that will be used to find and select matching
058 * items.
059 */
060 public AutoCompleteDocument(AbstractAutoCompleteAdaptor adaptor, boolean strictMatching) {
061 this.adaptor = adaptor;
062 this.strictMatching = strictMatching;
063
064 // Handle initially selected object
065 Object selected = adaptor.getSelectedItem();
066 if (selected!=null) setText(selected.toString());
067 adaptor.markEntireText();
068 }
069
070 /**
071 * Returns if only items from the adaptor's list should be allowed to be entered.
072 * @return if only items from the adaptor's list should be allowed to be entered
073 */
074 public boolean isStrictMatching() {
075 return strictMatching;
076 }
077
078 public void remove(int offs, int len) throws BadLocationException {
079 // return immediately when selecting an item
080 if (selecting) return;
081 super.remove(offs, len);
082 if (!strictMatching) {
083 setSelectedItem(getText(0, getLength()));
084 adaptor.getTextComponent().setCaretPosition(offs);
085 }
086 }
087
088 public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
089 // return immediately when selecting an item
090 if (selecting) return;
091 // insert the string into the document
092 super.insertString(offs, str, a);
093 // lookup and select a matching item
094 Object item = lookupItem(getText(0, getLength()));
095 if (item != null) {
096 setSelectedItem(item);
097 } else {
098 if (strictMatching) {
099 // keep old item selected if there is no match
100 item = adaptor.getSelectedItem();
101 // imitate no insert (later on offs will be incremented by
102 // str.length(): selection won't move forward)
103 offs = offs-str.length();
104 // provide feedback to the user that his input has been received but can not be accepted
105 UIManager.getLookAndFeel().provideErrorFeedback(adaptor.getTextComponent());
106 } else {
107 // no item matches => use the current input as selected item
108 item=getText(0, getLength());
109 setSelectedItem(item);
110 }
111 }
112 setText(item==null?"":item.toString());
113 // select the completed part
114 adaptor.markText(offs+str.length());
115 }
116
117 /**
118 * Sets the text of this AutoCompleteDocument to the given text.
119 *
120 * @param text the text that will be set for this document
121 */
122 private void setText(String text) {
123 try {
124 // remove all text and insert the completed string
125 super.remove(0, getLength());
126 super.insertString(0, text, null);
127 } catch (BadLocationException e) {
128 throw new RuntimeException(e.toString());
129 }
130 }
131
132 /**
133 * Selects the given item using the AbstractAutoCompleteAdaptor.
134 *
135 * @param item the item that is to be selected
136 */
137 private void setSelectedItem(Object item) {
138 selecting = true;
139 adaptor.setSelectedItem(item);
140 selecting = false;
141 }
142
143 /**
144 * Searches for an item that matches the given pattern. The AbstractAutoCompleteAdaptor
145 * is used to access the candidate items. The match is not case-sensitive
146 * and will only match at the beginning of each item's string representation.
147 *
148 * @param pattern the pattern that should be matched
149 * @return the first item that matches the pattern or <code>null</code> if no item matches
150 */
151 private Object lookupItem(String pattern) {
152 Object selectedItem = adaptor.getSelectedItem();
153 // only search for a different item if the currently selected does not match
154 if (selectedItem != null && startsWithIgnoreCase(selectedItem.toString(), pattern)) {
155 return selectedItem;
156 } else {
157 // iterate over all items
158 for (int i=0, n=adaptor.getItemCount(); i < n; i++) {
159 Object currentItem = adaptor.getItem(i);
160 // current item starts with the pattern?
161 if (currentItem != null && startsWithIgnoreCase(currentItem.toString(), pattern)) {
162 return currentItem;
163 }
164 }
165 }
166 // no item starts with the pattern => return null
167 return null;
168 }
169
170 /**
171 * Returns true if <code>base</code> starts with <code>prefix</code> (ignoring case).
172 * @param base the string to be checked
173 * @param prefix the prefix to check for
174 * @return true if <code>base</code> starts with <code>prefix</code>; false otherwise
175 */
176 private boolean startsWithIgnoreCase(String base, String prefix) {
177 if (base.length() < prefix.length()) return false;
178 return base.regionMatches(true, 0, prefix, 0, prefix.length());
179 }
180 }