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 }