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 }