001 /*
002 * Copyright 2002-2004 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005 * use this file except in compliance with the License. You may obtain a copy of
006 * the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013 * License for the specific language governing permissions and limitations under
014 * the License.
015 */
016 package org.springframework.richclient.list;
017
018 import java.awt.event.FocusEvent;
019 import java.awt.event.FocusListener;
020 import java.awt.event.KeyAdapter;
021 import java.awt.event.KeyEvent;
022 import java.util.HashMap;
023 import java.util.Map;
024
025 import javax.swing.ComboBoxModel;
026 import javax.swing.JComboBox;
027 import javax.swing.UIManager;
028 import javax.swing.event.ListDataEvent;
029 import javax.swing.event.ListDataListener;
030 import javax.swing.text.AttributeSet;
031 import javax.swing.text.BadLocationException;
032 import javax.swing.text.JTextComponent;
033 import javax.swing.text.PlainDocument;
034
035 import org.springframework.util.Assert;
036
037 /**
038 * Provides AutoCompletion to a combobox. Works with the editor of the JComboBox
039 * to make the conversion between strings and the objects of the JComboBox
040 * model. <br>
041 * Based on code contributed to the public domain by Thomas Bierhance
042 * (http://www.orbital-computer.de/JComboBox/)
043 *
044 * @author Peter De Bruycker
045 * @author Thomas Bierhance
046 */
047 public class ComboBoxAutoCompletion extends PlainDocument {
048
049 private final ChangeHandler changeHandler = new ChangeHandler();
050
051 private final JComboBox comboBox;
052
053 private final JTextComponent editor;
054
055 boolean hitBackspace;
056
057 boolean hitBackspaceOnSelection;
058
059 private Map item2string = new HashMap();
060
061 private ComboBoxModel model;
062
063 private boolean selectingValue;
064
065 /**
066 * Adds autocompletion support to the given <code>JComboBox</code>.
067 *
068 * @param comboBox
069 * the combobox
070 */
071 public ComboBoxAutoCompletion(final JComboBox comboBox) {
072 Assert.notNull(comboBox, "The ComboBox cannot be null.");
073 Assert.isTrue(!comboBox.isEditable(), "The ComboBox must not be editable.");
074 Assert.isTrue(comboBox.getEditor().getEditorComponent() instanceof JTextComponent,
075 "Only ComboBoxes with JTextComponent as editor are supported.");
076
077 this.comboBox = comboBox;
078 comboBox.setEditable(true);
079
080 model = comboBox.getModel();
081 model.addListDataListener(changeHandler);
082
083 editor = (JTextComponent)comboBox.getEditor().getEditorComponent();
084 editor.setDocument(this);
085 editor.addFocusListener(changeHandler);
086 editor.addKeyListener(changeHandler);
087
088 fillItem2StringMap();
089
090 // Handle initially selected object
091 Object selected = comboBox.getSelectedItem();
092 comboBox.getEditor().setItem(selected);
093 }
094
095 private void fillItem2StringMap() {
096 editor.setDocument(new PlainDocument());
097
098 item2string.clear();
099
100 JTextComponent editor = (JTextComponent)comboBox.getEditor().getEditorComponent();
101
102 // get current item of editor
103 Object currentItem = comboBox.getEditor().getItem();
104 for (int i = 0; i < comboBox.getItemCount(); i++) {
105 Object item = comboBox.getItemAt(i);
106 comboBox.getEditor().setItem(item);
107 item2string.put(item, editor.getText());
108 }
109 // reset item in editor
110 comboBox.getEditor().setItem(currentItem);
111
112 editor.setDocument(this);
113 }
114
115 private String getStringFor(Object item) {
116 return (String)item2string.get(item);
117 }
118
119 private void highlightCompletedText(int start) {
120 editor.setCaretPosition(getLength());
121 editor.moveCaretPosition(start);
122 }
123
124 /**
125 * @see javax.swing.text.Document#insertString(int, java.lang.String,
126 * javax.swing.text.AttributeSet)
127 */
128 public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
129 // ignore empty insert
130 if (str == null || str.length() == 0)
131 return;
132 if(selectingValue)
133 return;
134 // check offset position
135 if (offs < 0 || offs > getLength())
136 throw new BadLocationException("Invalid offset - must be >= 0 and <= " + getLength(), offs);
137
138 // construct the resulting string
139 String currentText = getText(0, getLength());
140 String beforeOffset = currentText.substring(0, offs);
141 String afterOffset = currentText.substring(offs, currentText.length());
142 String futureText = beforeOffset + str + afterOffset;
143
144 // lookup and select a matching item
145 Object item = lookupItem(futureText);
146 if (item != null) {
147 selectingValue = true;
148 try {
149 comboBox.setSelectedItem(item);
150 } finally {
151 selectingValue = false;
152 }
153 }
154 else {
155 // keep old item selected if there is no match
156 item = comboBox.getSelectedItem();
157 // imitate no insert (later on offs will be incremented by
158 // str.length(): selection won't move forward)
159 offs = offs - str.length();
160 // provide feedback to the user that his input has been received but
161 // can not be accepted
162 // comboBox.getToolkit().beep();
163 // when available use:
164 UIManager.getLookAndFeel().provideErrorFeedback(comboBox);
165 }
166
167 // display the completed string
168 String itemString = item == null ? "" : getStringFor(item);
169 setText(itemString);
170
171 // if the user selects an item via mouse the the whole string will be
172 // inserted.
173 // highlight the entire text if this happens.
174 if (itemString != null) {
175 if (itemString.equals(str) && offs == 0) {
176 highlightCompletedText(0);
177 }
178 else {
179 highlightCompletedText(offs + str.length());
180 // show popup when the user types
181 if (comboBox.isShowing()) {
182 comboBox.setPopupVisible(true);
183 }
184 }
185 }
186 }
187
188 private Object lookupItem(String pattern) {
189 Object selectedItem = model.getSelectedItem();
190 // only search for a different item if the currently selected does not
191 // match
192 if (selectedItem != null && startsWithIgnoreCase(getStringFor(selectedItem), pattern)) {
193 return selectedItem;
194 }
195
196 // iterate over all items
197 for (int i = 0, n = model.getSize(); i < n; i++) {
198 Object currentItem = model.getElementAt(i);
199 // current item starts with the pattern?
200 if (startsWithIgnoreCase(getStringFor(currentItem), pattern)) {
201 return currentItem;
202 }
203 }
204
205 // no item starts with the pattern => return null
206 return null;
207 }
208
209 /**
210 * @see javax.swing.text.Document#remove(int, int)
211 */
212 public void remove(int offs, int length) throws BadLocationException {
213 // ignore no deletion
214 if (length == 0)
215 return;
216 // check positions
217 if (offs < 0 || offs > getLength() || length < 0 || (offs + length) > getLength())
218 throw new BadLocationException("Invalid parameters.", offs);
219
220 if (hitBackspace) {
221 // user hit backspace => move the selection backwards
222 // old item keeps being selected
223 if (offs > 0) {
224 if (hitBackspaceOnSelection)
225 offs--;
226 }
227 else {
228 // User hit backspace with the cursor positioned on the start =>
229 // beep
230 comboBox.getToolkit().beep();
231 // when available use:
232 // UIManager.getLookAndFeel().provideErrorFeedback(comboBox);
233 }
234 highlightCompletedText(offs);
235 // show popup when the user types
236 if (comboBox.isShowing())
237 comboBox.setPopupVisible(true);
238 }
239 else {
240 super.remove(offs, length);
241 }
242 }
243
244 private void setText(String text) throws BadLocationException {
245 // remove all text and insert the new text
246 super.remove(0, getLength());
247 super.insertString(0, text, null);
248 }
249
250 // checks if str1 starts with str2 - ignores case
251 private boolean startsWithIgnoreCase(String str1, String str2) {
252 return str1 != null && str2 != null && str1.toUpperCase().startsWith(str2.toUpperCase());
253 }
254
255 private final class ChangeHandler extends KeyAdapter implements FocusListener, ListDataListener {
256
257 // Highlight whole text when user hits enter
258 // Register when user hits backspace
259 public void keyPressed(KeyEvent e) {
260 hitBackspace = false;
261 switch (e.getKeyCode()) {
262 case KeyEvent.VK_ENTER:
263 highlightCompletedText(0);
264 break;
265 // determine if the pressed key is backspace (needed by the remove
266 // method)
267 case KeyEvent.VK_BACK_SPACE:
268 hitBackspace = true;
269 hitBackspaceOnSelection = editor.getSelectionStart() != editor.getSelectionEnd();
270 break;
271 // ignore delete key
272 case KeyEvent.VK_DELETE:
273 e.consume();
274 ComboBoxAutoCompletion.this.comboBox.getToolkit().beep();
275 break;
276 }
277 }
278
279 // Bug 5100422 on Java 1.5: Editable JComboBox won't hide popup when
280 // tabbing out
281 private boolean hidePopupOnFocusLoss = System.getProperty("java.version").startsWith("1.5");
282
283 public void focusGained(FocusEvent e) {
284 // Highlight whole text when gaining focus
285 highlightCompletedText(0);
286 }
287
288 public void focusLost(FocusEvent e) {
289 // Workaround for Bug 5100422 - Hide Popup on focus loss
290 if (hidePopupOnFocusLoss)
291 ComboBoxAutoCompletion.this.comboBox.setPopupVisible(false);
292 }
293
294 public void contentsChanged(ListDataEvent e) {
295 if(!selectingValue)
296 fillItem2StringMap();
297 }
298
299 public void intervalAdded(ListDataEvent e) {
300 fillItem2StringMap();
301 }
302
303 public void intervalRemoved(ListDataEvent e) {
304 fillItem2StringMap();
305 }
306 }
307 }