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    }