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}