001: /*
002: * @(#)AutoCompletion.java 6/22/2005
003: *
004: * Copyright 2002 - 2005 JIDE Software Inc. All rights reserved.
005: */
006: package com.jidesoft.swing;
007:
008: import com.jidesoft.utils.SystemInfo;
009:
010: import javax.swing.*;
011: import javax.swing.event.ListSelectionEvent;
012: import javax.swing.event.ListSelectionListener;
013: import javax.swing.event.TreeSelectionEvent;
014: import javax.swing.event.TreeSelectionListener;
015: import javax.swing.text.*;
016: import javax.swing.tree.TreePath;
017: import java.awt.event.*;
018: import java.beans.PropertyChangeEvent;
019: import java.beans.PropertyChangeListener;
020: import java.util.List;
021:
022: /**
023: * <code>AutoCompletion</code> is a helper class to make JTextComponent or JComboBox auto-complete based
024: * on a list of known items.
025: * <p/>
026: * There are three constructors. The simplest one is {@link #AutoCompletion(javax.swing.JComboBox)}.
027: * It takes any combobox and make it auto completion. If you are looking for an auto-complete combobox solution,
028: * this is all you need. However <code>AutoCompletion</code> can do more than that. There are two more constrcutors.
029: * One is {@link #AutoCompletion(javax.swing.text.JTextComponent,Searchable)}.
030: * It will use {@link Searchable} which is another component available in JIDE to
031: * make the JTextCompoent auto-complete. We used Searchable here because it provides
032: * a common interface to access the element in JTree, JList or JTable. In
033: * the other word, the known list item we used to auto-complete can be got
034: * from JTree or JList or even JTable or any other component as
035: * long as it has Searchable interface implemented.
036: * The last constrcutor takes any java.util.List and use it as auto completion list.
037: * <p/>
038: * The only option available on <code>AutoCompletion</code> is {@link #setStrict(boolean)}. If it's true, it will not allow
039: * user to type in anything that is not in the known item list. If false, user can type in whatever he/she wants. If the text
040: * can match with a item in the known item list, it will still auto-complete.
041: * <p/>
042: *
043: * @author Thomas Bierhance
044: * @author JIDE Software, Inc.
045: */
046: public class AutoCompletion {
047:
048: private Searchable _searchable;
049: private JTextComponent _textComponent;
050:
051: private AutoCompletionDocument _document;
052:
053: // flag to indicate if setSelectedItem has been called
054: // subsequent calls to remove/insertString should be ignored
055: private boolean _selecting = false;
056:
057: private boolean _hidePopupOnFocusLoss;
058: private boolean _hitBackspace = false;
059: private boolean _hitBackspaceOnSelection;
060:
061: private KeyListener _editorKeyListener;
062: private FocusListener _editorFocusListener;
063:
064: private boolean _strict = true;
065: private boolean _strictCompletion = true;
066: private PropertyChangeListener _propertyChangeListener;
067: private JComboBox _comboBox;
068: private Document _oldDocument;
069:
070: public AutoCompletion(final JComboBox comboBox) {
071: this (comboBox, new ComboBoxSearchable(comboBox));
072: }
073:
074: public AutoCompletion(final JComboBox comboBox,
075: Searchable searchable) {
076: _searchable = searchable;
077: _propertyChangeListener = new PropertyChangeListener() {
078: public void propertyChange(PropertyChangeEvent e) {
079: if ("editor".equals(e.getPropertyName())) {
080: if (e.getNewValue() != null) {
081: _textComponent = (JTextComponent) ((ComboBoxEditor) e
082: .getNewValue()).getEditorComponent();
083: configureEditor(getTextComponent());
084: }
085: }
086: }
087: };
088: _comboBox = comboBox;
089: _searchable.setWildcardEnabled(false);
090: if (_searchable instanceof ComboBoxSearchable) {
091: ((ComboBoxSearchable) _searchable)
092: .setShowPopupDuringSearching(false);
093: }
094: _textComponent = (JTextComponent) comboBox.getEditor()
095: .getEditorComponent();
096: installListeners();
097: setInitValue();
098: }
099:
100: public AutoCompletion(final JTextComponent textComponent,
101: final Searchable searchable) {
102: _searchable = searchable;
103: _searchable.setWildcardEnabled(false);
104: _textComponent = textComponent;
105: registerSelectionListener(getSearchable());
106:
107: installListeners();
108: setInitValue();
109: }
110:
111: public AutoCompletion(final JTextComponent textComponent,
112: final List list) {
113: this (textComponent, new Searchable(new JLabel()) {
114: int _selectIndex = -1;
115:
116: @Override
117: protected int getSelectedIndex() {
118: return _selectIndex;
119: }
120:
121: @Override
122: protected void setSelectedIndex(int index,
123: boolean incremental) {
124: _selectIndex = index;
125: }
126:
127: @Override
128: protected int getElementCount() {
129: return list.size();
130: }
131:
132: @Override
133: protected Object getElementAt(int index) {
134: return list.get(index);
135: }
136:
137: @Override
138: protected String convertElementToString(Object element) {
139: return "" + element;
140: }
141: });
142: }
143:
144: public AutoCompletion(final JTextComponent textComponent,
145: final Object[] array) {
146: this (textComponent, new Searchable(new JLabel()) {
147: int _selectIndex = -1;
148:
149: @Override
150: protected int getSelectedIndex() {
151: return _selectIndex;
152: }
153:
154: @Override
155: protected void setSelectedIndex(int index,
156: boolean incremental) {
157: _selectIndex = index;
158: }
159:
160: @Override
161: protected int getElementCount() {
162: return array.length;
163: }
164:
165: @Override
166: protected Object getElementAt(int index) {
167: return array[index];
168: }
169:
170: @Override
171: protected String convertElementToString(Object element) {
172: return "" + element;
173: }
174: });
175: }
176:
177: private void registerSelectionListener(Searchable searchable) {
178: if (searchable.getComponent() instanceof JList) {
179: final JList list = (JList) getSearchable().getComponent();
180: list.getSelectionModel().addListSelectionListener(
181: new ListSelectionListener() {
182: public void valueChanged(ListSelectionEvent e) {
183: int index = list.getSelectedIndex();
184: if (index != -1) {
185: getTextComponent().setText(
186: ""
187: + list.getModel()
188: .getElementAt(
189: index));
190: highlightCompletedText(0);
191: }
192: }
193: });
194: DelegateAction.replaceAction(getTextComponent(),
195: JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT,
196: list, JComponent.WHEN_FOCUSED, KeyStroke
197: .getKeyStroke(KeyEvent.VK_UP, 0));
198: DelegateAction.replaceAction(getTextComponent(),
199: JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT,
200: list, JComponent.WHEN_FOCUSED, KeyStroke
201: .getKeyStroke(KeyEvent.VK_DOWN, 0));
202: DelegateAction.replaceAction(getTextComponent(),
203: JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT,
204: list, JComponent.WHEN_FOCUSED, KeyStroke
205: .getKeyStroke(KeyEvent.VK_PAGE_UP, 0));
206: DelegateAction.replaceAction(getTextComponent(),
207: JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT,
208: list, JComponent.WHEN_FOCUSED, KeyStroke
209: .getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0));
210: } else if (searchable.getComponent() instanceof JTree) {
211: final JTree tree = (JTree) getSearchable().getComponent();
212: tree.getSelectionModel().addTreeSelectionListener(
213: new TreeSelectionListener() {
214: public void valueChanged(TreeSelectionEvent e) {
215: TreePath treePath = tree.getSelectionPath();
216: if (treePath != null) {
217: getTextComponent()
218: .setText(
219: ""
220: + treePath
221: .getLastPathComponent());
222: highlightCompletedText(0);
223: }
224: }
225: });
226: DelegateAction.replaceAction(getTextComponent(),
227: JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT,
228: tree, JComponent.WHEN_FOCUSED, KeyStroke
229: .getKeyStroke(KeyEvent.VK_UP, 0));
230: DelegateAction.replaceAction(getTextComponent(),
231: JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT,
232: tree, JComponent.WHEN_FOCUSED, KeyStroke
233: .getKeyStroke(KeyEvent.VK_DOWN, 0));
234: DelegateAction.replaceAction(getTextComponent(),
235: JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT,
236: tree, JComponent.WHEN_FOCUSED, KeyStroke
237: .getKeyStroke(KeyEvent.VK_PAGE_UP, 0));
238: DelegateAction.replaceAction(getTextComponent(),
239: JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT,
240: tree, JComponent.WHEN_FOCUSED, KeyStroke
241: .getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0));
242: }
243: }
244:
245: private void setInitValue() {
246: int index = getSearchable().getSelectedIndex();
247: if (index != -1) {
248: Object selected = getSearchable().getElementAt(index);
249: if (selected != null)
250: _document.setText(getSearchable()
251: .convertElementToString(selected));
252: highlightCompletedText(0);
253: } else {
254: _document.setText("");
255: }
256: }
257:
258: /**
259: * Uninstalls the listeners so that the component is not auto-completion anymore.
260: */
261: public void uninstallListeners() {
262: if (_propertyChangeListener != null && _comboBox != null) {
263: _comboBox
264: .removePropertyChangeListener(_propertyChangeListener);
265: }
266:
267: if (getTextComponent() != null) {
268: getTextComponent().removeKeyListener(_editorKeyListener);
269: getTextComponent()
270: .removeFocusListener(_editorFocusListener);
271: String text = getTextComponent().getText();
272: if (_oldDocument != null) {
273: getTextComponent().setDocument(_oldDocument);
274: _oldDocument = null;
275: }
276: getTextComponent().setText(text);
277: }
278: }
279:
280: /**
281: * Installs the listeners needed for auto-completion feature.
282: * Please note, this method is already called when you create AutoCompletion.
283: * Unless you called {@link #uninstallListeners()}, there is no need to call this method yourself.
284: */
285: public void installListeners() {
286: if (_comboBox != null && _propertyChangeListener != null) {
287: _comboBox
288: .addPropertyChangeListener(_propertyChangeListener);
289: }
290:
291: _editorKeyListener = new KeyAdapter() {
292: private boolean _deletePressed;
293: private String _saveText;
294:
295: @Override
296: public void keyPressed(KeyEvent e) {
297: _hitBackspace = false;
298: switch (e.getKeyCode()) {
299: // determine if the pressed key is backspace (needed by the remove method)
300: case KeyEvent.VK_BACK_SPACE:
301: if (isStrict()) {
302: _hitBackspace = true;
303: _hitBackspaceOnSelection = getTextComponent()
304: .getSelectionStart() != getTextComponent()
305: .getSelectionEnd();
306: }
307: break;
308: // ignore delete key
309: case KeyEvent.VK_DELETE:
310: if (isStrict()) {
311: _deletePressed = true;
312: _saveText = getTextComponent().getText();
313: }
314: break;
315: }
316: }
317:
318: @Override
319: public void keyReleased(KeyEvent e) {
320: super .keyReleased(e);
321:
322: if (_deletePressed) {
323: _deletePressed = false;
324: String text = getTextComponent().getText();
325: int index = getSearchable().findFirst(text);
326: if (index != -1) {
327: Object item = getSearchable().getElementAt(
328: index);
329: setSelectedItem(item);
330: getTextComponent().setText(
331: getSearchable().convertElementToString(
332: item)); // this is what auto complete is
333: // select the completed part
334: highlightCompletedText(text.length());
335: } else { // didn't find a matching one
336: if (isStrict()) {
337: getTextComponent().setText(_saveText);
338: e.consume();
339: UIManager.getLookAndFeel()
340: .provideErrorFeedback(
341: _textComponent);
342: }
343: }
344: }
345: }
346: };
347: // Bug 5100422 on Java 1.5: Editable JComboBox won't hide popup when tabbing out
348: _hidePopupOnFocusLoss = SystemInfo.isJdk15Above();
349: // Highlight whole text when gaining focus
350: _editorFocusListener = new FocusAdapter() {
351: @Override
352: public void focusGained(FocusEvent e) {
353: highlightCompletedText(0);
354: }
355:
356: @Override
357: public void focusLost(FocusEvent e) {
358: // Workaround for Bug 5100422 - Hide Popup on focus loss
359: // if (_hidePopupOnFocusLoss) comboBox.setPopupVisible(false);
360: }
361: };
362:
363: _document = createDocument();
364: configureEditor(getTextComponent());
365: }
366:
367: /**
368: * Creates AutoCompletionDocument.
369: *
370: * @return the AutoCompletionDocument.
371: */
372: protected AutoCompletionDocument createDocument() {
373: return new AutoCompletionDocument();
374: }
375:
376: private void configureEditor(JTextComponent textComponent) {
377: if (getTextComponent() != null) {
378: getTextComponent().removeKeyListener(_editorKeyListener);
379: getTextComponent()
380: .removeFocusListener(_editorFocusListener);
381: }
382:
383: if (textComponent != null) {
384: _textComponent = textComponent;
385: getTextComponent().addKeyListener(_editorKeyListener);
386: getTextComponent().addFocusListener(_editorFocusListener);
387: String text = getTextComponent().getText();
388: _oldDocument = getTextComponent().getDocument();
389: getTextComponent().setDocument(_document);
390: getTextComponent().setText(text);
391: }
392: }
393:
394: /**
395: * The document class used by <tt>AutoCompletion</tt>.
396: */
397: protected class AutoCompletionDocument extends PlainDocument {
398: @Override
399: public void remove(int offs, int len)
400: throws BadLocationException {
401: // return immediately when _selecting an item
402: if (_selecting)
403: return;
404: if (_hitBackspace) {
405: // user hit backspace => move the selection backwards
406: // old item keeps being selected
407: if (offs > 0) {
408: if (_hitBackspaceOnSelection)
409: offs--;
410: } else {
411: // User hit backspace with the cursor positioned on the start => beep
412: UIManager.getLookAndFeel().provideErrorFeedback(
413: _textComponent);
414: }
415: highlightCompletedText(offs);
416: } else {
417: super .remove(offs, len);
418: }
419: }
420:
421: @Override
422: public void insertString(int offs, String str, AttributeSet a)
423: throws BadLocationException {
424: // return immediately when _selecting an item
425: if (_selecting)
426: return;
427: // insert the string into the document
428: super .insertString(offs, str, a);
429: // lookup and select a matching item
430: final String text = getText(0, getLength());
431: int index = getSearchable().findFromCursor(text);
432: Object item = null;
433: if (index != -1) {
434: item = getSearchable().getElementAt(index);
435: setSelectedItem(item);
436: setText(getSearchable().convertElementToString(item)); // this is what auto complete is
437: // select the completed part
438: highlightCompletedText(offs + str.length());
439: } else { // didn't find a matching one
440: if (isStrict()) {
441: index = getSearchable().getSelectedIndex();
442: if (index == -1) {
443: if (getSearchable().getElementCount() > 0) {
444: index = 0;
445: getSearchable().setSelectedIndex(0, false);
446: }
447: }
448:
449: if (index != -1) {
450: item = getSearchable().getElementAt(index);
451: offs = offs - str.length();
452: // imitate no insert (later on offs will be incremented by str.length(): selection won't move forward)
453: UIManager.getLookAndFeel()
454: .provideErrorFeedback(_textComponent);
455: setText(getSearchable().convertElementToString(
456: item));
457: // select the completed part
458: highlightCompletedText(offs + str.length());
459: }
460: }
461: }
462: }
463:
464: protected void setText(String text) {
465: try {
466: // remove all text and insert the completed string
467: if (isStrictCompletion()) {
468: super .remove(0, getLength());
469: super .insertString(0, text, null);
470: } else {
471: String existingText = super .getText(0, getLength());
472: int matchIndex = existingText.length();
473: // try to find a match
474: for (int i = 0; i < existingText.length(); i++) {
475: if (!existingText.substring(0, matchIndex)
476: .equalsIgnoreCase(
477: text.substring(0, matchIndex))) {
478: matchIndex--;
479: }
480: }
481: // remove the no-match part and complete with the one in Searchable
482: super .remove(matchIndex, getLength() - matchIndex);
483: super .insertString(matchIndex, text
484: .substring(matchIndex), null);
485: }
486: } catch (BadLocationException e) {
487: throw new RuntimeException(e.toString());
488: }
489: }
490: }
491:
492: private void highlightCompletedText(int start) {
493: getTextComponent().setCaretPosition(
494: getTextComponent().getDocument().getLength());
495: getTextComponent().moveCaretPosition(start);
496: }
497:
498: private void setSelectedItem(Object item) {
499: _selecting = true;
500: for (int i = 0, n = getSearchable().getElementCount(); i < n; i++) {
501: Object currentItem = getSearchable().getElementAt(i);
502: // current item starts with the pattern?
503: if (item == currentItem) {
504: getSearchable().setSelectedIndex(i, false);
505: }
506: }
507: _selecting = false;
508: }
509:
510: /**
511: * Gets the strict property.
512: *
513: * @return the value of strict property.
514: */
515: public boolean isStrict() {
516: return _strict;
517: }
518:
519: /**
520: * Sets the strict property. If true, it will not allow user to type in anything
521: * that is not in the known item list. If false, user can type in whatever he/she wants. If the text
522: * can match with a item in the known item list, it will still auto-complete.
523: *
524: * @param strict
525: */
526: public void setStrict(boolean strict) {
527: _strict = strict;
528: }
529:
530: /**
531: * Gets the strict completion property.
532: *
533: * @return the value of strict completion property.
534: * @see #setStrictCompletion(boolean)
535: */
536: public boolean isStrictCompletion() {
537: return _strictCompletion;
538: }
539:
540: /**
541: * Sets the strict completion property. If true, in case insensitive searching,
542: * it will always use the exact item in the Searchable to replace whatever user types. For example,
543: * when Searchable has an item "Arial" and user types in "AR", if this flag is true, it will autocompleted
544: * as "Arial". If false, it will be autocompleted as "ARial". Of course, this flag will only
545: * make a difference if Searchable is case insensitive.
546: *
547: * @param strictCompletion
548: */
549: public void setStrictCompletion(boolean strictCompletion) {
550: _strictCompletion = strictCompletion;
551: }
552:
553: /**
554: * Gets the underlying text component which auto-completes.
555: *
556: * @return the underlying text component.
557: */
558: protected JTextComponent getTextComponent() {
559: return _textComponent;
560: }
561:
562: /**
563: * Gets the underlying Searchable. If you use the constructor {@link #AutoCompletion(javax.swing.text.JTextComponent,Searchable)}, the return value
564: * will be the Searcable you passed in. If you use the other two constrcutors, internally we will
565: * still create a Searchable. If so, this Searchable will be returned.
566: *
567: * @return the Searchable.
568: */
569: public Searchable getSearchable() {
570: return _searchable;
571: }
572: }
|