001 /*
002 * $Id: AutoCompleteDocument.java 3365 2009-06-17 01:52:57Z kschaefe $
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 static org.jdesktop.swingx.autocomplete.ObjectToStringConverter.DEFAULT_IMPLEMENTATION;
024
025 import javax.swing.UIManager;
026 import javax.swing.event.DocumentEvent;
027 import javax.swing.event.DocumentListener;
028 import javax.swing.event.EventListenerList;
029 import javax.swing.event.UndoableEditEvent;
030 import javax.swing.event.UndoableEditListener;
031 import javax.swing.text.AttributeSet;
032 import javax.swing.text.BadLocationException;
033 import javax.swing.text.Document;
034 import javax.swing.text.Element;
035 import javax.swing.text.PlainDocument;
036 import javax.swing.text.Position;
037 import javax.swing.text.Segment;
038
039 import org.jdesktop.swingx.util.Contract;
040
041 /**
042 * A document that can be plugged into any JTextComponent to enable automatic completion.
043 * It finds and selects matching items using any implementation of the AbstractAutoCompleteAdaptor.
044 */
045 public class AutoCompleteDocument implements Document {
046 private class Handler implements DocumentListener, UndoableEditListener {
047 private final EventListenerList listenerList = new EventListenerList();
048
049 public void addDocumentListener(DocumentListener listener) {
050 listenerList.add(DocumentListener.class, listener);
051 }
052
053 public void addUndoableEditListener(UndoableEditListener listener) {
054 listenerList.add(UndoableEditListener.class, listener);
055 }
056
057 /**
058 * {@inheritDoc}
059 */
060 public void removeDocumentListener(DocumentListener listener) {
061 listenerList.remove(DocumentListener.class, listener);
062 }
063
064 /**
065 * {@inheritDoc}
066 */
067 public void removeUndoableEditListener(UndoableEditListener listener) {
068 listenerList.remove(UndoableEditListener.class, listener);
069 }
070
071 /**
072 * {@inheritDoc}
073 */
074 public void changedUpdate(DocumentEvent e) {
075 e = new DelegatingDocumentEvent(AutoCompleteDocument.this, e);
076
077 // Guaranteed to return a non-null array
078 Object[] listeners = listenerList.getListenerList();
079 // Process the listeners last to first, notifying
080 // those that are interested in this event
081 for (int i = listeners.length-2; i>=0; i-=2) {
082 if (listeners[i]==DocumentListener.class) {
083 // Lazily create the event:
084 // if (e == null)
085 // e = new ListSelectionEvent(this, firstIndex, lastIndex);
086 ((DocumentListener)listeners[i+1]).changedUpdate(e);
087 }
088 }
089 }
090
091 /**
092 * {@inheritDoc}
093 */
094 public void insertUpdate(DocumentEvent e) {
095 e = new DelegatingDocumentEvent(AutoCompleteDocument.this, e);
096
097 // Guaranteed to return a non-null array
098 Object[] listeners = listenerList.getListenerList();
099 // Process the listeners last to first, notifying
100 // those that are interested in this event
101 for (int i = listeners.length-2; i>=0; i-=2) {
102 if (listeners[i]==DocumentListener.class) {
103 // Lazily create the event:
104 // if (e == null)
105 // e = new ListSelectionEvent(this, firstIndex, lastIndex);
106 ((DocumentListener)listeners[i+1]).insertUpdate(e);
107 }
108 }
109 }
110
111 /**
112 * {@inheritDoc}
113 */
114 public void removeUpdate(DocumentEvent e) {
115 e = new DelegatingDocumentEvent(AutoCompleteDocument.this, e);
116
117 // Guaranteed to return a non-null array
118 Object[] listeners = listenerList.getListenerList();
119 // Process the listeners last to first, notifying
120 // those that are interested in this event
121 for (int i = listeners.length-2; i>=0; i-=2) {
122 if (listeners[i]==DocumentListener.class) {
123 // Lazily create the event:
124 // if (e == null)
125 // e = new ListSelectionEvent(this, firstIndex, lastIndex);
126 ((DocumentListener)listeners[i+1]).removeUpdate(e);
127 }
128 }
129 }
130
131 /**
132 * {@inheritDoc}
133 */
134 public void undoableEditHappened(UndoableEditEvent e) {
135 e = new UndoableEditEvent(AutoCompleteDocument.this, e.getEdit());
136
137 // Guaranteed to return a non-null array
138 Object[] listeners = listenerList.getListenerList();
139 // Process the listeners last to first, notifying
140 // those that are interested in this event
141 for (int i = listeners.length-2; i>=0; i-=2) {
142 if (listeners[i]==UndoableEditListener.class) {
143 // Lazily create the event:
144 // if (e == null)
145 // e = new ListSelectionEvent(this, firstIndex, lastIndex);
146 ((UndoableEditListener)listeners[i+1]).undoableEditHappened(e);
147 }
148 }
149 }
150 }
151
152 /** Flag to indicate if adaptor.setSelectedItem has been called.
153 * Subsequent calls to remove/insertString should be ignored
154 * as they are likely have been caused by the adapted Component that
155 * is trying to set the text for the selected component.*/
156 boolean selecting=false;
157
158 /**
159 * true, if only items from the adaptors's list can be entered
160 * false, otherwise (selected item might not be in the adaptors's list)
161 */
162 protected boolean strictMatching;
163
164 /**
165 * The adaptor that is used to find and select items.
166 */
167 AbstractAutoCompleteAdaptor adaptor;
168
169 ObjectToStringConverter stringConverter;
170
171 private final Handler handler;
172
173 protected final Document delegate;
174
175 /**
176 * Creates a new AutoCompleteDocument for the given AbstractAutoCompleteAdaptor.
177 * @param adaptor The adaptor that will be used to find and select matching
178 * items.
179 * @param strictMatching true, if only items from the adaptor's list should
180 * be allowed to be entered
181 * @param stringConverter the converter used to transform items to strings
182 * @param delegate the {@code Document} delegate backing this document
183 */
184 public AutoCompleteDocument(AbstractAutoCompleteAdaptor adaptor, boolean strictMatching,
185 ObjectToStringConverter stringConverter, Document delegate) {
186 this.adaptor = Contract.asNotNull(adaptor, "adaptor cannot be null");
187 this.strictMatching = strictMatching;
188 this.stringConverter = stringConverter == null ? DEFAULT_IMPLEMENTATION : stringConverter;
189 this.delegate = delegate == null ? createDefaultDocument() : delegate;
190
191 handler = new Handler();
192 this.delegate.addDocumentListener(handler);
193
194 // Handle initially selected object
195 Object selected = adaptor.getSelectedItem();
196 if (selected!=null) setText(this.stringConverter.getPreferredStringForItem(selected));
197 this.adaptor.markEntireText();
198 }
199
200
201 /**
202 * Creates a new AutoCompleteDocument for the given AbstractAutoCompleteAdaptor.
203 * @param adaptor The adaptor that will be used to find and select matching
204 * items.
205 * @param strictMatching true, if only items from the adaptor's list should
206 * be allowed to be entered
207 * @param stringConverter the converter used to transform items to strings
208 */
209 public AutoCompleteDocument(AbstractAutoCompleteAdaptor adaptor, boolean strictMatching, ObjectToStringConverter stringConverter) {
210 this(adaptor, strictMatching, stringConverter, null);
211 }
212
213 /**
214 * Creates a new AutoCompleteDocument for the given AbstractAutoCompleteAdaptor.
215 * @param strictMatching true, if only items from the adaptor's list should
216 * be allowed to be entered
217 * @param adaptor The adaptor that will be used to find and select matching
218 * items.
219 */
220 public AutoCompleteDocument(AbstractAutoCompleteAdaptor adaptor, boolean strictMatching) {
221 this(adaptor, strictMatching, null);
222 }
223
224 /**
225 * Creates the default backing document when no delegate is passed to this
226 * document.
227 *
228 * @return the default backing document
229 */
230 protected Document createDefaultDocument() {
231 return new PlainDocument();
232 }
233
234 public void remove(int offs, int len) throws BadLocationException {
235 // return immediately when selecting an item
236 if (selecting) return;
237 delegate.remove(offs, len);
238 if (!strictMatching) {
239 setSelectedItem(getText(0, getLength()), getText(0, getLength()));
240 adaptor.getTextComponent().setCaretPosition(offs);
241 }
242 }
243
244 public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
245 // return immediately when selecting an item
246 if (selecting) return;
247 // insert the string into the document
248 delegate.insertString(offs, str, a);
249 // lookup and select a matching item
250 LookupResult lookupResult;
251 String pattern = getText(0, getLength());
252
253 if(pattern == null || pattern.length() == 0) {
254 lookupResult = new LookupResult(null, "");
255 setSelectedItem(lookupResult.matchingItem, lookupResult.matchingString);
256 } else {
257 lookupResult = lookupItem(pattern);
258 if (lookupResult.matchingItem != null) {
259 setSelectedItem(lookupResult.matchingItem, lookupResult.matchingString);
260 } else {
261 if (strictMatching) {
262 // keep old item selected if there is no match
263 lookupResult.matchingItem = adaptor.getSelectedItem();
264 lookupResult.matchingString = adaptor.getSelectedItemAsString();
265 // imitate no insert (later on offs will be incremented by
266 // str.length(): selection won't move forward)
267 offs = offs-str.length();
268 // provide feedback to the user that his input has been received but can not be accepted
269 UIManager.getLookAndFeel().provideErrorFeedback(adaptor.getTextComponent());
270 } else {
271 // no item matches => use the current input as selected item
272 lookupResult.matchingItem=getText(0, getLength());
273 lookupResult.matchingString=getText(0, getLength());
274 setSelectedItem(lookupResult.matchingItem, lookupResult.matchingString);
275 }
276 }
277 }
278 setText(lookupResult.matchingString);
279 // select the completed part
280 adaptor.markText(offs+str.length());
281 }
282
283 /**
284 * Sets the text of this AutoCompleteDocument to the given text.
285 *
286 * @param text the text that will be set for this document
287 */
288 private void setText(String text) {
289 try {
290 // remove all text and insert the completed string
291 delegate.remove(0, getLength());
292 delegate.insertString(0, text, null);
293 } catch (BadLocationException e) {
294 throw new RuntimeException(e.toString());
295 }
296 }
297
298 /**
299 * Selects the given item using the AbstractAutoCompleteAdaptor.
300 * @param itemAsString string representation of the item to be selected
301 * @param item the item that is to be selected
302 */
303 private void setSelectedItem(Object item, String itemAsString) {
304 selecting = true;
305 adaptor.setSelectedItem(item);
306 adaptor.setSelectedItemAsString(itemAsString);
307 selecting = false;
308 }
309
310 /**
311 * Searches for an item that matches the given pattern. The AbstractAutoCompleteAdaptor
312 * is used to access the candidate items. The match is not case-sensitive
313 * and will only match at the beginning of each item's string representation.
314 *
315 * @param pattern the pattern that should be matched
316 * @return the first item that matches the pattern or <code>null</code> if no item matches
317 */
318 private LookupResult lookupItem(String pattern) {
319 Object selectedItem = adaptor.getSelectedItem();
320
321 String[] possibleStrings;
322
323 // iterate over all items to find an exact match
324 for (int i=0, n=adaptor.getItemCount(); i < n; i++) {
325 Object currentItem = adaptor.getItem(i);
326 possibleStrings = stringConverter.getPossibleStringsForItem(currentItem);
327 if (possibleStrings!=null) {
328 // current item exactly matches the pattern?
329 for (int j=0; j<possibleStrings.length; j++) {
330 if (possibleStrings[j].equalsIgnoreCase(pattern)) {
331 return new LookupResult(currentItem, possibleStrings[j]);
332 }
333 }
334 }
335 }
336 // check if the currently selected item matches
337 possibleStrings = stringConverter.getPossibleStringsForItem(selectedItem);
338 if (possibleStrings!=null) {
339 for (int i=0; i<possibleStrings.length; i++) {
340 if (startsWithIgnoreCase(possibleStrings[i], pattern)) {
341 return new LookupResult(selectedItem, possibleStrings[i]);
342 }
343 }
344 }
345 // search for any matching item, if the currently selected does not match
346 for (int i=0, n=adaptor.getItemCount(); i < n; i++) {
347 Object currentItem = adaptor.getItem(i);
348 possibleStrings = stringConverter.getPossibleStringsForItem(currentItem);
349 if (possibleStrings!=null) {
350 for (int j=0; j<possibleStrings.length; j++) {
351 if (startsWithIgnoreCase(possibleStrings[j], pattern)) {
352 return new LookupResult(currentItem, possibleStrings[j]);
353 }
354 }
355 }
356 }
357 // no item starts with the pattern => return null
358 return new LookupResult(null, "");
359 }
360
361 private static class LookupResult {
362 Object matchingItem;
363 String matchingString;
364 public LookupResult(Object matchingItem, String matchingString) {
365 this.matchingItem = matchingItem;
366 this.matchingString = matchingString;
367 }
368 }
369
370 /**
371 * Returns true if <code>base</code> starts with <code>prefix</code> (ignoring case).
372 * @param base the string to be checked
373 * @param prefix the prefix to check for
374 * @return true if <code>base</code> starts with <code>prefix</code>; false otherwise
375 */
376 //TODO entry point for handling 739
377 private boolean startsWithIgnoreCase(String base, String prefix) {
378 if (base.length() < prefix.length()) return false;
379 return base.regionMatches(true, 0, prefix, 0, prefix.length());
380 }
381
382 /**
383 * {@inheritDoc}
384 */
385 public void addDocumentListener(DocumentListener listener) {
386 handler.addDocumentListener(listener);
387 }
388
389 /**
390 * {@inheritDoc}
391 */
392 public void addUndoableEditListener(UndoableEditListener listener) {
393 handler.addUndoableEditListener(listener);
394 }
395
396 /**
397 * {@inheritDoc}
398 */
399 public Position createPosition(int offs) throws BadLocationException {
400 return delegate.createPosition(offs);
401 }
402
403 /**
404 * {@inheritDoc}
405 */
406 public Element getDefaultRootElement() {
407 return delegate.getDefaultRootElement();
408 }
409
410 /**
411 * {@inheritDoc}
412 */
413 public Position getEndPosition() {
414 return delegate.getEndPosition();
415 }
416
417 /**
418 * {@inheritDoc}
419 */
420 public int getLength() {
421 return delegate.getLength();
422 }
423
424 /**
425 * {@inheritDoc}
426 */
427 public Object getProperty(Object key) {
428 return delegate.getProperty(key);
429 }
430
431 /**
432 * {@inheritDoc}
433 */
434 public Element[] getRootElements() {
435 return delegate.getRootElements();
436 }
437
438 /**
439 * {@inheritDoc}
440 */
441 public Position getStartPosition() {
442 return delegate.getStartPosition();
443 }
444
445 /**
446 * {@inheritDoc}
447 */
448 public String getText(int offset, int length) throws BadLocationException {
449 return delegate.getText(offset, length);
450 }
451
452 /**
453 * {@inheritDoc}
454 */
455 public void getText(int offset, int length, Segment txt) throws BadLocationException {
456 delegate.getText(offset, length, txt);
457 }
458
459 /**
460 * {@inheritDoc}
461 */
462 public void putProperty(Object key, Object value) {
463 delegate.putProperty(key, value);
464 }
465
466 /**
467 * {@inheritDoc}
468 */
469 public void removeDocumentListener(DocumentListener listener) {
470 handler.removeDocumentListener(listener);
471 }
472
473 /**
474 * {@inheritDoc}
475 */
476 public void removeUndoableEditListener(UndoableEditListener listener) {
477 handler.removeUndoableEditListener(listener);
478 }
479
480 /**
481 * {@inheritDoc}
482 */
483 public void render(Runnable r) {
484 delegate.render(r);
485 }
486
487 /**
488 * Returns if only items from the adaptor's list should be allowed to be entered.
489 * @return if only items from the adaptor's list should be allowed to be entered
490 */
491 public boolean isStrictMatching() {
492 return strictMatching;
493 }
494 }