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