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 }