All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ca.odell.glazedlists.swing.AutoCompleteSupport Maven / Gradle / Ivy

The newest version!
/* Glazed Lists                                                 (c) 2003-2006 */
/* http://publicobject.com/glazedlists/                      publicobject.com,*/
/*                                                     O'Dell Engineering Ltd.*/
package ca.odell.glazedlists.swing;

import ca.odell.glazedlists.*;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.gui.TableFormat;
import ca.odell.glazedlists.impl.GlazedListsImpl;
import ca.odell.glazedlists.impl.filter.SearchTerm;
import ca.odell.glazedlists.impl.filter.TextMatcher;
import ca.odell.glazedlists.impl.swing.ComboBoxPopupLocationFix;
import ca.odell.glazedlists.matchers.Matcher;
import ca.odell.glazedlists.matchers.Matchers;
import ca.odell.glazedlists.matchers.TextMatcherEditor;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Method;
import java.text.Format;
import java.text.ParsePosition;
import java.util.Comparator;
import java.util.List;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.plaf.UIResource;
import javax.swing.plaf.basic.BasicComboBoxEditor;
import javax.swing.plaf.basic.ComboPopup;
import javax.swing.text.*;

/**
 * This class {@link #install}s support for filtering and autocompletion into
 * a standard {@link JComboBox}. It also acts as a factory class for
 * {@link #createTableCellEditor creating autocompleting table cell editors}.
 *
 * 

All autocompletion behaviour provided is meant to mimic the functionality * of the Firefox address field. To be explicit, the following is a list of * expected behaviours which are installed: * *

Typing into the ComboBox Editor *

    *
  1. typing any value into the editor when the popup is invisible causes * the popup to appear and its contents to be filtered according to the * editor's text. It also autocompletes to the first item that is * prefixed with the editor's text and selects that item within the popup.
  2. *
  3. typing any value into the editor when the popup is visible causes * the popup to be refiltered according to the editor's text and * reselects an appropriate autocompletion item.
  4. *
  5. typing the down or up arrow keys in the editor when the popup is * invisible causes the popup to appear and its contents to be filtered * according to the editor's text. It also autocompletes to the first * item that is prefixed with the editor's text and selects that item * within the popup.
  6. *
  7. typing the up arrow key when the popup is visible and the selected * element is the first element causes the autocompletion to be cleared * and the popup's selection to be removed.
  8. *
  9. typing the up arrow key when no selection exists causes the last * element of the popup to become selected and used for autocompletion
  10. *
  11. typing the down arrow key when the popup is visible and the selected * element is the last element causes the autocompletion to be cleared * and the popup's selection to be removed
  12. *
  13. typing the down arrow key when no selection exists causes the first * element of the popup to become selected and used for autocompletion
  14. *
  15. typing the delete key while in strict mode will select the prior * text rather than delete it. Attempting to delete all text results in * a beep unless the autcomplete support has been configured to not beep. *
* * Clicking on the Arrow Button *
    *
  1. clicking the arrow button when the popup is invisible causes the * popup to appear and its contents to be shown unfiltered *
  2. clicking the arrow button when the popup is visible causes the popup * to be hidden *
* * Sizing the ComboBox Popup *
    *
  1. the popup is always at least as wide as the * autocompleting {@link JComboBox}, but may be wider to accomodate a * {@link JComboBox#getPrototypeDisplayValue() prototype display value} * if a non-null prototype display value exists *
  2. as items are filtered in the ComboBoxModel, the popup height is * adjusted to display between 0 and {@link JComboBox#getMaximumRowCount()} * rows before scrolling the popup *
* * JComboBox ActionEvents *

A single {@link ActionEvent} is fired from the JComboBox in these situations: *

    *
  1. the user hits the enter key *
  2. the selected item within the popup is changed (which can happen due * to a mouse click, a change in the autocompletion term, or using the * arrow keys) *
  3. the JComboBox loses focus and contains a value that does not appear * in the ComboBoxModel *
* * Null Values in the ComboBoxModel *

null values located in the ComboBoxModel are considered * identical to empty Strings ("") for the purposes of locating autocompletion * terms.

* * ComboBoxEditor Focus *
    *
  1. the text in the ComboBoxEditor is selected if * {@link #getSelectsTextOnFocusGain()} returns true *
  2. the JPopupMenu is hidden when the ComboBoxEditor loses focus if * {@link #getHidesPopupOnFocusLost()} returns true *
* * Extracting String Values *

Each value in the ComboBoxModel must be converted to a String for many * reasons: filtering, setting into the ComboBoxEditor, displaying in the * renderer, etc. By default, JComboBox relies on {@link Object#toString()} * to map elements to their String equivalents. Sometimes, however, toString() * is not a reliable or desirable mechanism to use. To deal with this problem, * AutoCompleteSupport provides an install method that takes a {@link Format} * object which is used to do all converting back and forth between Strings and * ComboBoxModel objects.

* *

In order to achieve all of the autocompletion and filtering behaviour, * the following occurs when {@link #install} is called: * *

    *
  • the JComboBox will be made editable *
  • the JComboBox will have a custom ComboBoxModel installed on it * containing the given items *
  • the ComboBoxEditor will be wrapped with functionality and set back * into the JComboBox as the editor *
  • the JTextField which is the editor component for the JComboBox * will have a DocumentFilter installed on its backing Document *
* * The strategy of this support class is to alter all of the objects which * influence the behaviour of the JComboBox in one single context. With that * achieved, it greatly reduces the cross-functional communication required to * customize the behaviour of JComboBox for filtering and autocompletion. * *

Warning: This class must be * mutated from the Swing Event Dispatch Thread. Failure to do so will result in * an {@link IllegalStateException} thrown from any one of: * *

    *
  • {@link #install(JComboBox, EventList)} *
  • {@link #install(JComboBox, EventList, TextFilterator)} *
  • {@link #install(JComboBox, EventList, TextFilterator, Format)} *
  • {@link #isInstalled()} *
  • {@link #uninstall()} *
  • {@link #setCorrectsCase(boolean)} *
  • {@link #setStrict(boolean)} *
  • {@link #setBeepOnStrictViolation(boolean)} *
  • {@link #setSelectsTextOnFocusGain(boolean)} *
  • {@link #setHidesPopupOnFocusLost(boolean)} *
  • {@link #setFilterMode(int)} *
  • {@link #setFirstItem(Object)} *
  • {@link #removeFirstItem()} *
* * @author James Lemieux */ public final class AutoCompleteSupport { private static final ParsePosition PARSE_POSITION = new ParsePosition(0); private static final Class[] VALUE_OF_SIGNATURE = {String.class}; /** Marker object for indicating "not found". */ private static final Object NOT_FOUND = new Object(); // // These member variables control behaviour of the autocompletion support // /** * true if user-specified text is converted into the same case as * the autocompletion term. false will leave user specified text * unaltered. */ private boolean correctsCase = true; /** * false if the user can specify values that do not appear in the * ComboBoxModel; true otherwise. */ private boolean strict = false; /** * true indicates a beep sound should be played to the user to * indicate their error when attempting to violate the {@link #strict} * setting; false indicates we should not beep. */ private boolean beepOnStrictViolation = true; /** * true if the text in the combobox editor is selected when the * editor gains focus; false otherwise. */ private boolean selectsTextOnFocusGain = true; /** * true if the {@link #popupMenu} should always * be hidden when the {@link #comboBoxEditor} loses focus; false * if the default behaviour should be preserved. This exists to provide a * reasonable alternative to the strange default behaviour in JComboBox in * which the tab key will advance focus to the next focusable component and * leave the JPopupMenu visible. */ private boolean hidesPopupOnFocusLost = true; // // These are member variables for convenience // /** The comboBox being decorated with autocomplete functionality. */ private JComboBox comboBox; /** The popup menu of the decorated comboBox. */ private JPopupMenu popupMenu; /** The popup that wraps the popupMenu of the decorated comboBox. */ private ComboPopup popup; /** The arrow button that invokes the popup. */ private JButton arrowButton; /** The model backing the comboBox. */ private final AutoCompleteComboBoxModel comboBoxModel; /** The custom renderer installed on the comboBox or null if one is not required. */ private final ListCellRenderer renderer; /** The EventList which holds the items present in the comboBoxModel. */ private final EventList items; /** The FilterList which filters the items present in the comboBoxModel. */ private final FilterList filteredItems; /** A single-element EventList for storing the optional first element, typically used to represent "no selection". */ private final EventList firstItem; /** The CompositeList which is the union of firstItem and filteredItems to produce all filtered items available in the comboBoxModel. */ private final CompositeList allItemsFiltered; /** The CompositeList which is the union of firstItem and items to produce all unfiltered items available in the comboBoxModel. */ private final CompositeList allItemsUnfiltered; /** The Format capable of producing Strings from ComboBoxModel elements and vice versa. */ private final Format format; /** The MatcherEditor driving the FilterList behind the comboBoxModel. */ private final TextMatcherEditor filterMatcherEditor; /** * The custom ComboBoxEditor that does NOT assume that the text value can * be computed using Object.toString(). (i.e. the default ComboBoxEditor * *does* assume that, but we decorate it and remove that assumption) */ private FormatComboBoxEditor comboBoxEditor; /** The textfield which acts as the editor of the comboBox. */ private JTextField comboBoxEditorComponent; /** The Document backing the comboBoxEditorComponent. */ private AbstractDocument document; /** A DocumentFilter that controls edits to the document. */ private final AutoCompleteFilter documentFilter = new AutoCompleteFilter(); /** The last prefix specified by the user. */ private String prefix = ""; /** The Matcher that decides if a ComboBoxModel element is filtered out. */ private Matcher filterMatcher = Matchers.trueMatcher(); /** true while processing a text change to the {@link #comboBoxEditorComponent}; false otherwise. */ private boolean isFiltering = false; /** Controls the selection behavior of the JComboBox when it is used in a JTable DefaultCellEditor. */ private final boolean isTableCellEditor; // // These listeners work together to enforce different aspects of the autocompletion behaviour // /** * The MouseListener which is installed on the {@link #arrowButton} and * is responsible for clearing the filter and then showing / hiding the * {@link #popup}. */ private ArrowButtonMouseListener arrowButtonMouseListener; /** * A listener which reacts to changes in the ComboBoxModel by * resizing the popup appropriately to accomodate the new data. */ private final ListDataListener listDataHandler = new ListDataHandler(); /** * We ensure the popup menu is sized correctly each time it is shown. * Namely, we respect the prototype display value of the combo box, if * it has one. Regardless of the width of the combo box, we attempt to * size the popup to accomodate the width of the prototype display value. */ private final PopupMenuListener popupSizerHandler = new PopupSizer(); /** * An unfortunately necessary fixer for a misplaced popup. */ private ComboBoxPopupLocationFix popupLocationFix; /** * We ensure that selecting an item from the popup via the mouse never * attempts to autocomplete for fear that we will replace the user's * newly selected item and the item will effectively be unselectable. */ private final MouseListener popupMouseHandler = new PopupMouseHandler(); /** Handles the special case of the backspace key in strict mode and the enter key. */ private final KeyListener strictModeBackspaceHandler = new AutoCompleteKeyHandler(); /** Handles selecting the text in the comboBoxEditorComponent when it gains focus. */ private final FocusListener selectTextOnFocusGainHandler = new ComboBoxEditorFocusHandler(); // // These listeners watch for invalid changes to the JComboBox which break our autocompletion // /** * Watches for changes of the Document which backs comboBoxEditorComponent and uninstalls * our DocumentFilter from the old Document and reinstalls it on the new. */ private final DocumentWatcher documentWatcher = new DocumentWatcher(); /** Watches for changes of the ComboBoxModel and reports them as violations. */ private final ModelWatcher modelWatcher = new ModelWatcher(); /** Watches for changes of the ComboBoxUI and reinstalls the autocompletion support. */ private final UIWatcher uiWatcher = new UIWatcher(); // // These booleans control when certain changes are to be respected and when they aren't // /** true indicates document changes should not be post processed * (i.e. just commit changes to the Document and do not cause any side-effects). */ private boolean doNotPostProcessDocumentChanges = false; /** true indicates attempts to filter the ComboBoxModel should be ignored. */ private boolean doNotFilter = false; /** true indicates attempts to change the document should be ignored. */ private boolean doNotChangeDocument = false; /** true indicates attempts to select an autocompletion term should be ignored. */ private boolean doNotAutoComplete = false; /** true indicates attempts to toggle the state of the popup should be ignored. * In general, the only time we should toggle the state of a popup is due to a users keystroke * (and not programmatically setting the selected item, for example). * * When the JComboBox is used within a TableCellEditor, this value is ALWAYS false, since * we MUST accept keystrokes, even when they are passed second hand to the JComboBox after * it has been installed as the cell editor (as opposed to typed into the JComboBox directly) */ private boolean doNotTogglePopup; /** true indicates attempts to clear the filter when hiding the popup should be ignored. * This is because sometimes we hide and reshow a popup in rapid succession and we want to avoid * the work to unfiltering/refiltering it. */ private boolean doNotClearFilterOnPopupHide = false; // // Values present before {@link #install} executes - and are restored when {@link @uninstall} executes // /** The original setting of the editable field on the comboBox. */ private final boolean originalComboBoxEditable; /** The original model installed on the comboBox. */ private ComboBoxModel originalModel; /** The original ListCellRenderer installed on the comboBox. */ private ListCellRenderer originalRenderer; // // Values present before {@link #decorateCurrentUI} executes - and are restored when {@link @undecorateOriginalUI} executes // /** The original Actions associated with the up and down arrow keys. */ private Action originalSelectNextAction; private Action originalSelectPreviousAction; private Action originalSelectNext2Action; private Action originalSelectPrevious2Action; private Action originalAquaSelectNextAction; private Action originalAquaSelectPreviousAction; /** * This private constructor creates an AutoCompleteSupport object which adds * autocompletion functionality to the given comboBox. In * particular, a custom {@link ComboBoxModel} is installed behind the * comboBox containing the given items. The * filterator is consulted in order to extract searchable * text from each of the items. Non-null format * objects are used to convert ComboBoxModel elements to Strings and back * again for various functions like filtering, editing, and rendering. * * @param comboBox the {@link JComboBox} to decorate with autocompletion * @param items the objects to display in the comboBox * @param filterator extracts searchable text strings from each item * @param format converts combobox elements into strings and vice versa */ private AutoCompleteSupport(JComboBox comboBox, EventList items, TextFilterator filterator, Format format) { this.comboBox = comboBox; this.originalComboBoxEditable = comboBox.isEditable(); this.originalModel = comboBox.getModel(); this.items = items; this.format = format; // only build a custom renderer if the user specified their own Format but has not installed a custom renderer of their own final boolean defaultRendererInstalled = comboBox.getRenderer() instanceof UIResource; this.renderer = format != null && defaultRendererInstalled ? new StringFunctionRenderer() : null; // is this combo box a TableCellEditor? this.isTableCellEditor = Boolean.TRUE.equals(comboBox.getClientProperty("JComboBox.isTableCellEditor")); this.doNotTogglePopup = !isTableCellEditor; // lock the items list for reading since we want to prevent writes // from occurring until we fully initialize this AutoCompleteSupport items.getReadWriteLock().readLock().lock(); try { // build the ComboBoxModel capable of filtering its values this.filterMatcherEditor = new TextMatcherEditor(filterator == null ? new DefaultTextFilterator() : filterator); this.filterMatcherEditor.setMode(TextMatcherEditor.STARTS_WITH); this.filteredItems = new FilterList(items, this.filterMatcherEditor); this.firstItem = new BasicEventList(items.getPublisher(), items.getReadWriteLock()); // the ComboBoxModel always contains the firstItem and a filtered view of all other items this.allItemsFiltered = new CompositeList(items.getPublisher(), items.getReadWriteLock()); this.allItemsFiltered.addMemberList(this.firstItem); this.allItemsFiltered.addMemberList(this.filteredItems); this.comboBoxModel = new AutoCompleteComboBoxModel(this.allItemsFiltered); // we need an unfiltered view in order to try to locate autocompletion terms this.allItemsUnfiltered = new CompositeList(items.getPublisher(), items.getReadWriteLock()); this.allItemsUnfiltered.addMemberList(this.firstItem); this.allItemsUnfiltered.addMemberList(this.items); } finally { items.getReadWriteLock().readLock().unlock(); } // customize the comboBox this.comboBox.setModel(this.comboBoxModel); this.comboBox.setEditable(true); decorateCurrentUI(); // react to changes made to the key parts of JComboBox which affect autocompletion this.comboBox.addPropertyChangeListener("UI", this.uiWatcher); this.comboBox.addPropertyChangeListener("model", this.modelWatcher); this.comboBoxEditorComponent.addPropertyChangeListener("document", this.documentWatcher); } /** * A convenience method to ensure {@link AutoCompleteSupport} is being * accessed from the Event Dispatch Thread. */ private static void checkAccessThread() { if (!SwingUtilities.isEventDispatchThread()) throw new IllegalStateException("AutoCompleteSupport must be accessed from the Swing Event Dispatch Thread, but was called on Thread \"" + Thread.currentThread().getName() + "\""); } /** * A convenience method to unregister and return all {@link ActionListener}s * currently installed on the given comboBox. This is the only * technique we can rely on to prevent the comboBox from * broadcasting {@link ActionEvent}s at inappropriate times. * * This method is the logical inverse of {@link #registerAllActionListeners}. */ private static ActionListener[] unregisterAllActionListeners(JComboBox comboBox) { final ActionListener[] listeners = comboBox.getActionListeners(); for (int i = 0; i < listeners.length; i++) comboBox.removeActionListener(listeners[i]); return listeners; } /** * A convenience method to register all of the given listeners * with the given comboBox. * * This method is the logical inverse of {@link #unregisterAllActionListeners}. */ private static void registerAllActionListeners(JComboBox comboBox, ActionListener[] listeners) { for (int i = 0; i < listeners.length; i++) comboBox.addActionListener(listeners[i]); } /** * A convenience method to search through the given JComboBox for the * JButton which toggles the popup up open and closed. */ private static JButton findArrowButton(JComboBox c) { for (int i = 0, n = c.getComponentCount(); i < n; i++) { final Component comp = c.getComponent(i); if (comp instanceof JButton) return (JButton) comp; } return null; } /** * Decorate all necessary areas of the current UI to install autocompletion * support. This method is called in the constructor and when the comboBox's * UI delegate is changed. */ private void decorateCurrentUI() { // record some original settings of comboBox this.originalRenderer = comboBox.getRenderer(); this.popupMenu = (JPopupMenu) comboBox.getUI().getAccessibleChild(comboBox, 0); this.popup = (ComboPopup) popupMenu; this.arrowButton = findArrowButton(comboBox); // if an arrow button was found, decorate the ComboPopup's MouseListener // with logic that unfilters the ComboBoxModel when the arrow button is pressed if (this.arrowButton != null) { this.arrowButton.removeMouseListener(popup.getMouseListener()); this.arrowButtonMouseListener = new ArrowButtonMouseListener(popup.getMouseListener()); this.arrowButton.addMouseListener(arrowButtonMouseListener); } // start listening for model changes (due to filtering) so we can resize the popup vertically this.comboBox.getModel().addListDataListener(listDataHandler); // calculate the popup's width according to the prototype value, if one exists this.popupMenu.addPopupMenuListener(popupSizerHandler); // fix the popup's location this.popupLocationFix = ComboBoxPopupLocationFix.install(this.comboBox); // start suppressing autocompletion when selecting values from the popup with the mouse this.popup.getList().addMouseListener(popupMouseHandler); // record the original Up/Down arrow key Actions final ActionMap actionMap = comboBox.getActionMap(); this.originalSelectNextAction = actionMap.get("selectNext"); this.originalSelectPreviousAction = actionMap.get("selectPrevious"); this.originalSelectNext2Action = actionMap.get("selectNext2"); this.originalSelectPrevious2Action = actionMap.get("selectPrevious2"); this.originalAquaSelectNextAction = actionMap.get("aquaSelectNext"); this.originalAquaSelectPreviousAction = actionMap.get("aquaSelectPrevious"); final Action upAction = new MoveAction(-1); final Action downAction = new MoveAction(1); // install custom actions for the arrow keys in all non-Apple L&Fs actionMap.put("selectPrevious", upAction); actionMap.put("selectNext", downAction); actionMap.put("selectPrevious2", upAction); actionMap.put("selectNext2", downAction); // install custom actions for the arrow keys in the Apple Aqua L&F actionMap.put("aquaSelectPrevious", upAction); actionMap.put("aquaSelectNext", downAction); // install a custom ComboBoxEditor that decorates the existing one, but uses the // convertToString(...) method to produce text for the editor component (rather than .toString()) this.comboBoxEditor = new FormatComboBoxEditor(comboBox.getEditor()); this.comboBox.setEditor(comboBoxEditor); // add a DocumentFilter to the Document backing the editor JTextField this.comboBoxEditorComponent = (JTextField) comboBox.getEditor().getEditorComponent(); this.document = (AbstractDocument) comboBoxEditorComponent.getDocument(); this.document.setDocumentFilter(documentFilter); // install a custom renderer on the combobox, if we have built one if (this.renderer != null) comboBox.setRenderer(renderer); // add a KeyListener to the ComboBoxEditor which handles the special case of backspace when in strict mode this.comboBoxEditorComponent.addKeyListener(strictModeBackspaceHandler); // add a FocusListener to the ComboBoxEditor which selects all text when focus is gained this.comboBoxEditorComponent.addFocusListener(selectTextOnFocusGainHandler); } /** * Remove all customizations installed to various areas of the current UI * in order to uninstall autocompletion support. This method is invoked * after the comboBox's UI delegate is changed. */ private void undecorateOriginalUI() { // if an arrow button was found, remove our custom MouseListener and // reinstall the normal popup MouseListener if (this.arrowButton != null) { this.arrowButton.removeMouseListener(arrowButtonMouseListener); this.arrowButton.addMouseListener(arrowButtonMouseListener.getDecorated()); } // stop listening for model changes this.comboBox.getModel().removeListDataListener(listDataHandler); // remove the DocumentFilter from the Document backing the editor JTextField this.document.setDocumentFilter(null); // restore the original ComboBoxEditor if our custom ComboBoxEditor is still installed if (this.comboBox.getEditor() == comboBoxEditor) this.comboBox.setEditor(comboBoxEditor.getDelegate()); // stop adjusting the popup's width according to the prototype value this.popupMenu.removePopupMenuListener(popupSizerHandler); // stop fixing the combobox's popup location this.popupLocationFix.uninstall(); // stop suppressing autocompletion when selecting values from the popup with the mouse this.popup.getList().removeMouseListener(popupMouseHandler); final ActionMap actionMap = comboBox.getActionMap(); // restore the original actions for the arrow keys in all non-Apple L&Fs actionMap.put("selectPrevious", originalSelectPreviousAction); actionMap.put("selectNext", originalSelectNextAction); actionMap.put("selectPrevious2", originalSelectPrevious2Action); actionMap.put("selectNext2", originalSelectNext2Action); // restore the original actions for the arrow keys in the Apple Aqua L&F actionMap.put("aquaSelectPrevious", originalAquaSelectPreviousAction); actionMap.put("aquaSelectNext", originalAquaSelectNextAction); // remove the KeyListener from the ComboBoxEditor which handles the special case of backspace when in strict mode this.comboBoxEditorComponent.removeKeyListener(strictModeBackspaceHandler); // remove the FocusListener from the ComboBoxEditor which selects all text when focus is gained this.comboBoxEditorComponent.removeFocusListener(selectTextOnFocusGainHandler); // remove the custom renderer if it is still installed if (this.comboBox.getRenderer() == renderer) this.comboBox.setRenderer(originalRenderer); // erase some original settings of comboBox this.originalRenderer = null; this.comboBoxEditor = null; this.comboBoxEditorComponent = null; this.document = null; this.popupMenu = null; this.popup = null; this.arrowButton = null; } /** * Installs support for autocompletion into the comboBox and * returns the support object that is actually providing those facilities. * The support object is returned so that the caller may invoke * {@link #uninstall} at some later time to remove the autocompletion * features. * *

This method assumes that the items can be converted into * reasonable String representations via {@link Object#toString()}. * *

The following must be true in order to successfully install support * for autocompletion on a {@link JComboBox}: * *

    *
  • The JComboBox must use a {@link JTextField} as its editor component *
  • The JTextField must use an {@link AbstractDocument} as its model *
* * @param comboBox the {@link JComboBox} to decorate with autocompletion * @param items the objects to display in the comboBox * @return an instance of the support class providing autocomplete features * @throws IllegalStateException if this method is called from any Thread * other than the Swing Event Dispatch Thread */ public static AutoCompleteSupport install(JComboBox comboBox, EventList items) { return install(comboBox, items, null); } /** * Installs support for autocompletion into the comboBox and * returns the support object that is actually providing those facilities. * The support object is returned so that the caller may invoke * {@link #uninstall} at some later time to remove the autocompletion * features. * *

This method assumes that the items can be converted into * reasonable String representations via {@link Object#toString()}. * *

The filterator will be used to extract searchable text * strings from each of the items. A null * filterator implies the item's toString() method should be used when * filtering it. * *

The following must be true in order to successfully install support * for autocompletion on a {@link JComboBox}: * *

    *
  • The JComboBox must use a {@link JTextField} as its editor component *
  • The JTextField must use an {@link AbstractDocument} as its model *
* * @param comboBox the {@link JComboBox} to decorate with autocompletion * @param items the objects to display in the comboBox * @param filterator extracts searchable text strings from each item; * null implies the item's toString() method should be * used when filtering it * @return an instance of the support class providing autocomplete features * @throws IllegalStateException if this method is called from any Thread * other than the Swing Event Dispatch Thread */ public static AutoCompleteSupport install(JComboBox comboBox, EventList items, TextFilterator filterator) { return install(comboBox, items, filterator, null); } /** * Installs support for autocompletion into the comboBox and * returns the support object that is actually providing those facilities. * The support object is returned so that the caller may invoke * {@link #uninstall} at some later time to remove the autocompletion * features. * *

This method uses the given format to convert the * given items into Strings and back again. In other words, * this method does NOT rely on {@link Object#toString()} * to produce a reasonable String representation of each item. Likewise, * it does not rely on the existence of a valueOf(String) method for * creating items out of Strings as is the default behaviour of JComboBox. * *

It can be assumed that the only methods called on the given format are: *

    *
  • {@link Format#format(Object)} *
  • {@link Format#parseObject(String, ParsePosition)} *
* *

As a convenience, this method will install a custom * {@link ListCellRenderer} on the comboBox that displays the * String value returned by the format. Though this is only * done if the given format is not null and if * the comboBox does not already use a custom renderer. * *

The filterator will be used to extract searchable text * strings from each of the items. A null * filterator implies one of two default strategies will be used. If the * format is not null then the String value returned from the * format object will be used when filtering a given item. * Otherwise, the item's toString() method will be used when it is filtered. * *

The following must be true in order to successfully install support * for autocompletion on a {@link JComboBox}: * *

    *
  • The JComboBox must use a {@link JTextField} as its editor component *
  • The JTextField must use an {@link AbstractDocument} as its model *
* * @param comboBox the {@link JComboBox} to decorate with autocompletion * @param items the objects to display in the comboBox * @param filterator extracts searchable text strings from each item. If the * format is not null then the String value returned from * the format object will be used when filtering a given * item. Otherwise, the item's toString() method will be used when it * is filtered. * @param format a Format object capable of converting items * into Strings and back. null indicates the standard * JComboBox methods of converting are acceptable. * @return an instance of the support class providing autocomplete features * @throws IllegalStateException if this method is called from any Thread * other than the Swing Event Dispatch Thread */ public static AutoCompleteSupport install(JComboBox comboBox, EventList items, TextFilterator filterator, Format format) { checkAccessThread(); final Component editorComponent = comboBox.getEditor().getEditorComponent(); if (!(editorComponent instanceof JTextField)) throw new IllegalArgumentException("comboBox must use a JTextField as its editor component"); if (!(((JTextField) editorComponent).getDocument() instanceof AbstractDocument)) throw new IllegalArgumentException("comboBox must use a JTextField backed by an AbstractDocument as its editor component"); if (comboBox.getModel().getClass() == AutoCompleteSupport.AutoCompleteComboBoxModel.class) throw new IllegalArgumentException("comboBox is already configured for autocompletion"); return new AutoCompleteSupport(comboBox, items, filterator, format); } /** * This method is used to report environmental invariants which are * violated when the user adjusts the combo box in a way that is * incompatible with the requirements for autocompletion. A message can be * specified which will be included in the {@link IllegalStateException} * that is throw out of this method after the autocompletion support is * uninstalled. * * @param message a message to the programmer explaining the environmental * invariant that was violated */ private void throwIllegalStateException(String message) { final String exceptionMsg = message + "\n" + "In order for AutoCompleteSupport to continue to " + "work, the following invariants must be maintained after " + "AutoCompleteSupport.install() has been called:\n" + "* the ComboBoxModel may not be removed\n" + "* the AbstractDocument behind the JTextField can be changed but must be changed to some subclass of AbstractDocument\n" + "* the DocumentFilter on the AbstractDocument behind the JTextField may not be removed\n"; uninstall(); throw new IllegalStateException(exceptionMsg); } /** * A convenience method to produce a String from the given * comboBoxElement. */ private String convertToString(Object comboBoxElement) { if (comboBoxElement == NOT_FOUND) return "NOT_FOUND"; if (format != null) return format.format(comboBoxElement); return comboBoxElement == null ? "" : comboBoxElement.toString(); } /** * Returns the autocompleting {@link JComboBox} or null if * {@link AutoCompleteSupport} has been {@link #uninstall}ed. */ public JComboBox getComboBox() { return this.comboBox; } /** * Returns the {@link TextFilterator} that extracts searchable strings from * each item in the {@link ComboBoxModel}. */ public TextFilterator getTextFilterator() { return this.filterMatcherEditor.getFilterator(); } /** * Returns the filtered {@link EventList} of items which backs the * {@link ComboBoxModel} of the autocompleting {@link JComboBox}. */ public EventList getItemList() { return this.filteredItems; } /** * Returns true if user specified strings are converted to the * case of the autocompletion term they match; false otherwise. */ public boolean getCorrectsCase() { return correctsCase; } /** * If correctCase is true, user specified strings * will be converted to the case of the element they match. Otherwise * they will be left unaltered. * *

Note: this flag only has meeting when strict mode is turned off. * When strict mode is on, case is corrected regardless of this setting. * * @see #setStrict(boolean) * * @throws IllegalStateException if this method is called from any Thread * other than the Swing Event Dispatch Thread */ public void setCorrectsCase(boolean correctCase) { checkAccessThread(); this.correctsCase = correctCase; } /** * Returns true if the user is able to specify values which do not * appear in the popup list of suggestions; false otherwise. */ public boolean isStrict() { return strict; } /** * If strict is false, the user can specify values * not appearing within the ComboBoxModel. If it is true each * keystroke must continue to match some value in the ComboBoxModel or it * will be discarded. * *

Note: When strict mode is enabled, all user input is corrected to the * case of the autocompletion term, regardless of the correctsCase setting. * * @see #setCorrectsCase(boolean) * * @throws IllegalStateException if this method is called from any Thread * other than the Swing Event Dispatch Thread */ public void setStrict(boolean strict) { checkAccessThread(); if (this.strict == strict) return; this.strict = strict; // if strict mode was just turned on, ensure the comboBox contains a // value from the ComboBoxModel (i.e. start being strict!) if (strict) { final String currentText = comboBoxEditorComponent.getText(); Object currentItem = findAutoCompleteTerm(currentText); String currentItemText = convertToString(currentItem); boolean itemMatches = currentItem == comboBox.getSelectedItem(); boolean textMatches = GlazedListsImpl.equal(currentItemText, currentText); // select the first element if no autocompletion term could be found if (currentItem == NOT_FOUND && !allItemsUnfiltered.isEmpty()) { currentItem = allItemsUnfiltered.get(0); currentItemText = convertToString(currentItem); itemMatches = currentItem == comboBox.getSelectedItem(); textMatches = GlazedListsImpl.equal(currentItemText, currentText); } // return all elements to the ComboBoxModel applyFilter(""); doNotPostProcessDocumentChanges = true; try { // adjust the editor's text, if necessary if (!textMatches) comboBoxEditorComponent.setText(currentItemText); // adjust the model's selected item, if necessary if (!itemMatches || comboBox.getSelectedIndex() == -1) comboBox.setSelectedItem(currentItem); } finally { doNotPostProcessDocumentChanges = false; } } } /** * Returns true if a beep sound is played when the user attempts * to violate the strict invariant; false if no beep sound is * played. This setting is only respected if {@link #isStrict()} returns * true. * * @see #setStrict(boolean) */ public boolean getBeepOnStrictViolation() { return beepOnStrictViolation; } /** * Sets the policy for indicating strict-mode violations to the user by way * of a beep sound. * * @param beepOnStrictViolation true if a beep sound should be * played when the user attempts to violate the strict invariant; * false if no beep sound should be played * * @throws IllegalStateException if this method is called from any Thread * other than the Swing Event Dispatch Thread */ public void setBeepOnStrictViolation(boolean beepOnStrictViolation) { checkAccessThread(); this.beepOnStrictViolation = beepOnStrictViolation; } /** * Returns true if the combo box editor text is selected when it * gains focus; false otherwise. */ public boolean getSelectsTextOnFocusGain() { return selectsTextOnFocusGain; } /** * If selectsTextOnFocusGain is true, all text in the * editor is selected when the combo box editor gains focus. If it is * false the selection state of the editor is not effected by * focus changes. * * @throws IllegalStateException if this method is called from any Thread * other than the Swing Event Dispatch Thread */ public void setSelectsTextOnFocusGain(boolean selectsTextOnFocusGain) { checkAccessThread(); this.selectsTextOnFocusGain = selectsTextOnFocusGain; } /** * Returns true if the popup menu is hidden whenever the combo * box editor loses focus; false otherwise. */ public boolean getHidesPopupOnFocusLost() { return hidesPopupOnFocusLost; } /** * If hidesPopupOnFocusLost is true, then the popup * menu of the combo box is always hidden whenever the * combo box editor loses focus. If it is false the default * behaviour is preserved. In practice this means that if focus is lost * because of a MouseEvent, the behaviour is reasonable, but if focus is * lost because of a KeyEvent (e.g. tabbing to the next focusable component) * then the popup menu remains visible. * * @throws IllegalStateException if this method is called from any Thread * other than the Swing Event Dispatch Thread */ public void setHidesPopupOnFocusLost(boolean hidesPopupOnFocusLost) { checkAccessThread(); this.hidesPopupOnFocusLost = hidesPopupOnFocusLost; } /** * Returns the manner in which the contents of the {@link ComboBoxModel} * are filtered. This method will return one of * {@link TextMatcherEditor#CONTAINS} or {@link TextMatcherEditor#STARTS_WITH}. * *

{@link TextMatcherEditor#CONTAINS} indicates elements of the * {@link ComboBoxModel} are matched when they contain the text entered by * the user. * *

{@link TextMatcherEditor#STARTS_WITH} indicates elements of the * {@link ComboBoxModel} are matched when they start with the text entered * by the user. * *

In both modes, autocompletion only occurs when a given item starts * with user-specified text. The filter mode only affects the filtering * aspect of autocomplete support. */ public int getFilterMode() { return filterMatcherEditor.getMode(); } /** * Sets the manner in which the contents of the {@link ComboBoxModel} are * filtered. The given mode must be one of * {@link TextMatcherEditor#CONTAINS} or {@link TextMatcherEditor#STARTS_WITH}. * * @throws IllegalStateException if this method is called from any Thread * other than the Swing Event Dispatch Thread * * @see #getFilterMode() */ public void setFilterMode(int mode) { checkAccessThread(); // adjust the MatcherEditor that filters the AutoCompleteComboBoxModel to respect the given mode // but ONLY adjust the contents of the model, avoid changing the text in the JComboBox's textfield doNotChangeDocument = true; try { filterMatcherEditor.setMode(mode); } finally { doNotChangeDocument = false; } } /** * Sets the manner in which the contents of the {@link ComboBoxModel} are * filtered and autocompletion terms are matched. The given strategy must be one of * {@link TextMatcherEditor#IDENTICAL_STRATEGY} or {@link TextMatcherEditor#NORMALIZED_STRATEGY} * or the Unicode strategy of the ICU4J extension. * * @throws IllegalStateException if this method is called from any Thread * other than the Swing Event Dispatch Thread * * @see #getTextMatchingStrategy() */ public void setTextMatchingStrategy(Object strategy) { checkAccessThread(); // adjust the MatcherEditor that filters the AutoCompleteComboBoxModel to respect the given strategy // but ONLY adjust the contents of the model, avoid changing the text in the JComboBox's textfield doNotChangeDocument = true; try { filterMatcherEditor.setStrategy(strategy); // do we need to update the filterMatcher here? } finally { doNotChangeDocument = false; } } /** * Returns the manner in which the contents of the {@link ComboBoxModel} are * filtered and autocompletion terms are matched. The returned strategy is one of * {@link TextMatcherEditor#IDENTICAL_STRATEGY} or {@link TextMatcherEditor#NORMALIZED_STRATEGY} * or the Unicode strategy of the ICU4J extension. */ public Object getTextMatchingStrategy() { return filterMatcherEditor.getStrategy(); } /** * This method set a single optional value to be used as the first element * in the {@link ComboBoxModel}. This value typically represents * "no selection" or "blank". This value is always present and is not * filtered away during autocompletion. * * @param item the first value to present in the {@link ComboBoxModel} */ public void setFirstItem(E item) { checkAccessThread(); doNotChangeDocument = true; firstItem.getReadWriteLock().writeLock().lock(); try { if (firstItem.isEmpty()) firstItem.add(item); else firstItem.set(0, item); } finally { firstItem.getReadWriteLock().writeLock().unlock(); doNotChangeDocument = false; } } /** * Returns the optional single value used as the first element in the * {@link ComboBoxModel} or null if no first item has been set. * * @return the special first value presented in the {@link ComboBoxModel} * or null if no first item has been set */ public E getFirstItem() { firstItem.getReadWriteLock().readLock().lock(); try { return firstItem.isEmpty() ? null : firstItem.get(0); } finally { firstItem.getReadWriteLock().readLock().unlock(); } } /** * Removes and returns the optional single value used as the first element * in the {@link ComboBoxModel} or null if no first item has been * set. * * @return the special first value presented in the {@link ComboBoxModel} * or null if no first item has been set */ public E removeFirstItem() { checkAccessThread(); doNotChangeDocument = true; firstItem.getReadWriteLock().writeLock().lock(); try { return firstItem.isEmpty() ? null : firstItem.remove(0); } finally { firstItem.getReadWriteLock().writeLock().unlock(); doNotChangeDocument = false; } } /** * Returns true if this autocomplete support instance is currently * installed and altering the behaviour of the combo box; false if * it has been {@link #uninstall}ed. * * @throws IllegalStateException if this method is called from any Thread * other than the Swing Event Dispatch Thread */ public boolean isInstalled() { checkAccessThread(); return comboBox != null; } /** * This method removes autocompletion support from the {@link JComboBox} * it was installed on. This method is useful when the {@link EventList} of * items that backs the combo box must outlive the combo box itself. * Calling this method will return the combo box to its original state * before autocompletion was installed, and it will be available for * garbage collection independently of the {@link EventList} of items. * * @throws IllegalStateException if this method is called from any Thread * other than the Swing Event Dispatch Thread */ public void uninstall() { checkAccessThread(); if (this.comboBox == null) throw new IllegalStateException("This AutoCompleteSupport has already been uninstalled"); items.getReadWriteLock().readLock().lock(); try { // 1. stop listening for changes this.comboBox.removePropertyChangeListener("UI", this.uiWatcher); this.comboBox.removePropertyChangeListener("model", this.modelWatcher); this.comboBoxEditorComponent.removePropertyChangeListener("document", this.documentWatcher); // 2. undecorate the original UI components this.undecorateOriginalUI(); // 3. restore the original model to the JComboBox this.comboBox.setModel(originalModel); this.originalModel = null; // 4. restore the original editable flag to the JComboBox this.comboBox.setEditable(originalComboBoxEditable); // 5. dispose of our ComboBoxModel this.comboBoxModel.dispose(); // 6. dispose of our EventLists so that they are severed from the given items EventList this.allItemsFiltered.dispose(); this.allItemsUnfiltered.dispose(); this.filteredItems.dispose(); // null out the comboBox to indicate that this support class is uninstalled this.comboBox = null; } finally { items.getReadWriteLock().readLock().unlock(); } } /** * This method updates the value which filters the items in the * ComboBoxModel. * * @param newFilter the new value by which to filter the item */ private void applyFilter(String newFilter) { // break out early if we're flagged to ignore filter updates for the time being if (doNotFilter) return; // ignore attempts to change the text in the combo box editor while // the filtering is taking place doNotChangeDocument = true; final ActionListener[] listeners = unregisterAllActionListeners(comboBox); isFiltering = true; try { filterMatcherEditor.setFilterText(new String[] {newFilter}); } finally { isFiltering = false; registerAllActionListeners(comboBox, listeners); doNotChangeDocument = false; } } /** * This method updates the {@link #prefix} to be the current value in the * ComboBoxEditor. */ private void updateFilter() { prefix = comboBoxEditorComponent.getText(); if (prefix.length() == 0) filterMatcher = Matchers.trueMatcher(); else filterMatcher = new TextMatcher(new SearchTerm[] {new SearchTerm(prefix)}, GlazedLists.toStringTextFilterator(), TextMatcherEditor.STARTS_WITH, getTextMatchingStrategy()); } /** * A small convenience method to try showing the ComboBoxPopup. */ private void togglePopup() { // break out early if we're flagged to ignore attempts to toggle the popup state if (doNotTogglePopup) return; if (comboBoxModel.getSize() == 0) comboBox.hidePopup(); else if (comboBox.isShowing() && !comboBox.isPopupVisible() && comboBoxEditorComponent.hasFocus()) comboBox.showPopup(); } /** * Performs a linear scan of ALL ITEMS, regardless of the filtering state * of the ComboBoxModel, to locate the autocomplete term. If an exact * match of the given value can be found, then the item is * returned. If an exact match cannot be found, the first term that * starts with the given value is returned. * *

If no exact or partial match can be located, null is * returned. */ private Object findAutoCompleteTerm(String value) { // determine if our value is empty final boolean prefixIsEmpty = "".equals(value); final Matcher valueMatcher = new TextMatcher(new SearchTerm[] {new SearchTerm(value)}, GlazedLists.toStringTextFilterator(), TextMatcherEditor.STARTS_WITH, getTextMatchingStrategy()); Object partialMatchItem = NOT_FOUND; // search the list of ALL UNFILTERED items for an autocompletion term for the given value for (int i = 0, n = allItemsUnfiltered.size(); i < n; i++) { final E item = allItemsUnfiltered.get(i); final String itemString = convertToString(item); // if we have an exact match, return the given value immediately if (value.equals(itemString)) return item; // if we have not yet located a partial match, check the current itemString for a partial match // (to be returned if an exact match cannot be found) if (partialMatchItem == NOT_FOUND) { if (prefixIsEmpty ? "".equals(itemString) : valueMatcher.matches(itemString)) partialMatchItem = item; } } return partialMatchItem; } /** * This special version of EventComboBoxModel simply marks a flag to * indicate the items in the ComboBoxModel should not be filtered as a * side-effect of setting the selected item. It also marks another flag * to indicate that the selected item is being explicitly set, and thus * autocompletion should not execute and possibly overwrite the * programmer's specified value. */ private class AutoCompleteComboBoxModel extends EventComboBoxModel { public AutoCompleteComboBoxModel(EventList source) { super(source); } /** * Overridden because AutoCompleteSupport needs absolute control over * when a JComboBox's ActionListeners are notified. */ @Override public void setSelectedItem(Object selected) { doNotFilter = true; doNotAutoComplete = true; // remove all ActionListeners from the JComboBox since setting the selected item // would normally notify them, but in normal autocompletion behaviour, we don't want that final ActionListener[] listeners = unregisterAllActionListeners(comboBox); try { super.setSelectedItem(selected); if (comboBoxEditorComponent != null) { // remove any text selection that might exist when an item is selected final int caretPos = comboBoxEditorComponent.getCaretPosition(); comboBoxEditorComponent.select(caretPos, caretPos); } } finally { // reinstall the ActionListeners we removed registerAllActionListeners(comboBox, listeners); doNotFilter = false; doNotAutoComplete = false; } } /** * Overridden because ListEvents produce ListDataEvents from this * ComboBoxModel, which notify the BasicComboBoxUI of the data change, * which in turn tries to set the text of the ComboBoxEditor to match * the text of the selected item. We don't want that. AutoCompleteSupport * is the ultimate authority on the text value in the ComboBoxEditor. * We override this method to set doNotChangeDocument to ensure that * attempts to change the ComboBoxEditor's Document are ignored and * our control is absolute. */ @Override public void listChanged(ListEvent listChanges) { doNotChangeDocument = true; try { super.listChanged(listChanges); } finally { doNotChangeDocument = false; } } } /** * This class is the crux of the entire solution. This custom DocumentFilter * controls all edits which are attempted against the Document of the * ComboBoxEditor component. It is our hook to either control when to respect * edits as well as the side-effects the edit has on autocompletion and * filtering. */ private class AutoCompleteFilter extends DocumentFilter { @Override public void replace(FilterBypass filterBypass, int offset, int length, String string, AttributeSet attributeSet) throws BadLocationException { if (doNotChangeDocument) return; // collect rollback information before performing the replace final String valueBeforeEdit = comboBoxEditorComponent.getText(); final int selectionStart = comboBoxEditorComponent.getSelectionStart(); final int selectionEnd = comboBoxEditorComponent.getSelectionEnd(); // this short-circuit corrects the PlasticLookAndFeel behaviour. Hitting the enter key in Plastic // will cause the popup to reopen because the Plastic ComboBoxEditor forwards on unnecessary updates // to the document, including ones where the text isn't really changing final boolean isReplacingAllText = offset == 0 && document.getLength() == length; if (isReplacingAllText && valueBeforeEdit.equals(string)) return; super.replace(filterBypass, offset, length, string, attributeSet); postProcessDocumentChange(filterBypass, attributeSet, valueBeforeEdit, selectionStart, selectionEnd, true); } @Override public void insertString(FilterBypass filterBypass, int offset, String string, AttributeSet attributeSet) throws BadLocationException { if (doNotChangeDocument) return; // collect rollback information before performing the insert final String valueBeforeEdit = comboBoxEditorComponent.getText(); final int selectionStart = comboBoxEditorComponent.getSelectionStart(); final int selectionEnd = comboBoxEditorComponent.getSelectionEnd(); super.insertString(filterBypass, offset, string, attributeSet); postProcessDocumentChange(filterBypass, attributeSet, valueBeforeEdit, selectionStart, selectionEnd, true); } @Override public void remove(FilterBypass filterBypass, int offset, int length) throws BadLocationException { if (doNotChangeDocument) return; // collect rollback information before performing the remove final String valueBeforeEdit = comboBoxEditorComponent.getText(); final int selectionStart = comboBoxEditorComponent.getSelectionStart(); final int selectionEnd = comboBoxEditorComponent.getSelectionEnd(); super.remove(filterBypass, offset, length); postProcessDocumentChange(filterBypass, null, valueBeforeEdit, selectionStart, selectionEnd, isStrict()); } /** * This method generically post processes changes to the ComboBox * editor's Document. The generic algorithm, regardless of the type of * change, is as follows: * *

    *
  1. save the prefix as the user has entered it *
  2. filter the combo box items against the prefix *
  3. update the text in the combo box editor with an autocomplete suggestion *
  4. try to show the popup, if possible *
*/ private void postProcessDocumentChange(FilterBypass filterBypass, AttributeSet attributeSet, String valueBeforeEdit, int selectionStart, int selectionEnd, boolean allowPartialAutoCompletionTerm) throws BadLocationException { // break out early if we're flagged to not post process the Document change if (doNotPostProcessDocumentChanges) return; final String valueAfterEdit = comboBoxEditorComponent.getText(); // if an autocomplete term could not be found and we're in strict mode, rollback the edit if (isStrict() && (findAutoCompleteTerm(valueAfterEdit) == NOT_FOUND) && !allItemsUnfiltered.isEmpty()) { // indicate the error to the user if (getBeepOnStrictViolation()) UIManager.getLookAndFeel().provideErrorFeedback(comboBoxEditorComponent); // rollback the edit doNotPostProcessDocumentChanges = true; try { comboBoxEditorComponent.setText(valueBeforeEdit); } finally { doNotPostProcessDocumentChanges = false; } // restore the selection as it existed comboBoxEditorComponent.select(selectionStart, selectionEnd); // do not continue post processing changes return; } // record the selection before post processing the Document change // (we'll use this to decide whether to broadcast an ActionEvent when choosing the next selected index) final Object selectedItemBeforeEdit = comboBox.getSelectedItem(); updateFilter(); applyFilter(prefix); selectAutoCompleteTerm(filterBypass, attributeSet, selectedItemBeforeEdit, allowPartialAutoCompletionTerm); togglePopup(); } /** * This method will attempt to locate a reasonable autocomplete item * from all combo box items and select it. It will also populate the * combo box editor with the remaining text which matches the * autocomplete item and select it. If the selection changes and the * JComboBox is not a Table Cell Editor, an ActionEvent will be * broadcast from the combo box. */ private void selectAutoCompleteTerm(FilterBypass filterBypass, AttributeSet attributeSet, Object selectedItemBeforeEdit, boolean allowPartialAutoCompletionTerm) throws BadLocationException { // break out early if we're flagged to ignore attempts to autocomplete if (doNotAutoComplete) return; // determine if our prefix is empty (in which case we cannot use our filterMatcher to locate an autocompletion term) final boolean prefixIsEmpty = "".equals(prefix); // record the original caret position in case we don't want to disturb the text (occurs when an exact autocomplete term match is found) final int originalCaretPosition = comboBoxEditorComponent.getCaretPosition(); // a flag to indicate whether a partial match or exact match exists on the autocomplete term boolean autoCompleteTermIsExactMatch = false; // search the combobox model for a value that starts with our prefix (called an autocompletion term) for (int i = 0, n = comboBoxModel.getSize(); i < n; i++) { String itemString = convertToString(comboBoxModel.getElementAt(i)); // if itemString does not match the prefix, continue searching for an autocompletion term if (prefixIsEmpty ? !"".equals(itemString) : !filterMatcher.matches(itemString)) continue; // record the index and value that are our "best" autocomplete terms so far int matchIndex = i; String matchString = itemString; // search for an *exact* match in the remainder of the ComboBoxModel // before settling for the partial match we have just found for (int j = i; j < n; j++) { itemString = convertToString(comboBoxModel.getElementAt(j)); // if we've located an exact match, use its index and value rather than the partial match if (prefix.equals(itemString)) { matchIndex = j; matchString = itemString; autoCompleteTermIsExactMatch = true; break; } } // if partial autocompletion terms are not allowed, and we only have a partial term, bail early if (!allowPartialAutoCompletionTerm && !prefix.equals(itemString)) return; // either keep the user's prefix or replace it with the itemString's prefix // depending on whether we correct the case if (getCorrectsCase() || isStrict()) { filterBypass.replace(0, prefix.length(), matchString, attributeSet); } else { final String itemSuffix = matchString.substring(prefix.length()); filterBypass.insertString(prefix.length(), itemSuffix, attributeSet); } // select the autocompletion term final boolean silently = isTableCellEditor || GlazedListsImpl.equal(selectedItemBeforeEdit, matchString); selectItem(matchIndex, silently); if (autoCompleteTermIsExactMatch) { // if the term matched the original text exactly, return the caret to its original location comboBoxEditorComponent.setCaretPosition(originalCaretPosition); } else { // select the text after the prefix but before the end of the text (it represents the autocomplete text) comboBoxEditorComponent.select(prefix.length(), document.getLength()); } return; } // reset the selection since we couldn't find the prefix in the model // (this has the side-effect of scrolling the popup to the top) final boolean silently = isTableCellEditor || selectedItemBeforeEdit == null; selectItem(-1, silently); } /** * Select the item at the given index. If * silent is true, the JComboBox will not * broadcast an ActionEvent. */ private void selectItem(int index, boolean silently) { final Object valueToSelect = index == -1 ? null : comboBoxModel.getElementAt(index); // if nothing is changing about the selection, return immediately if (GlazedListsImpl.equal(comboBoxModel.getSelectedItem(), valueToSelect)) return; doNotChangeDocument = true; try { if (silently) comboBoxModel.setSelectedItem(valueToSelect); else comboBox.setSelectedItem(valueToSelect); } finally { doNotChangeDocument = false; } } } /** * Select the item at the given index. This method behaves * differently in strict mode vs. non-strict mode. * *

In strict mode, the selected index must always be valid, so using the * down arrow key on the last item or the up arrow key on the first item * simply wraps the selection to the opposite end of the model. * *

In non-strict mode, the selected index can be -1 (no selection), so we * allow -1 to mean "adjust the value of the ComboBoxEditor to be the user's * text" and only wrap to the end of the model when -2 is reached. In short, * -1 is interpreted as "clear the selected item". * -2 is interpreted as "the last element". */ private void selectPossibleValue(int index) { if (isStrict()) { // wrap the index from past the start to the end of the model if (index < 0) index = comboBox.getModel().getSize()-1; // wrap the index from past the end to the start of the model if (index > comboBox.getModel().getSize()-1) index = 0; } else { // wrap the index from past the start to the end of the model if (index == -2) index = comboBox.getModel().getSize()-1; } // check if the index is within a valid range final boolean validIndex = index >= 0 && index < comboBox.getModel().getSize(); // if the index isn't valid, select nothing if (!validIndex) index = -1; // adjust only the value in the comboBoxEditorComponent, but leave the comboBoxModel unchanged doNotPostProcessDocumentChanges = true; try { // select the index if (isTableCellEditor) { // while operating as a TableCellEditor, no ActionListeners must be notified // when using the arrow keys to adjust the selection final ActionListener[] listeners = unregisterAllActionListeners(comboBox); try { comboBox.setSelectedIndex(index); } finally { registerAllActionListeners(comboBox, listeners); } } else { comboBox.setSelectedIndex(index); } // if the original index wasn't valid, we've cleared the selection // and must set the user's prefix into the editor if (!validIndex) { comboBoxEditorComponent.setText(prefix); // don't bother unfiltering the popup since we'll redisplay the popup immediately doNotClearFilterOnPopupHide = true; try { comboBox.hidePopup(); } finally { doNotClearFilterOnPopupHide = false; } comboBox.showPopup(); } } finally { doNotPostProcessDocumentChanges = false; } // if the comboBoxEditorComponent's values begins with the user's prefix, highlight the remainder of the value final String newSelection = comboBoxEditorComponent.getText(); if (filterMatcher.matches(newSelection)) comboBoxEditorComponent.select(prefix.length(), newSelection.length()); } /** * The action invoked by hitting the up or down arrow key. */ private class MoveAction extends AbstractAction { private final int offset; public MoveAction(int offset) { this.offset = offset; } public void actionPerformed(ActionEvent e) { if (comboBox.isShowing()) { if (comboBox.isPopupVisible()) { selectPossibleValue(comboBox.getSelectedIndex() + offset); } else { applyFilter(prefix); comboBox.showPopup(); } } } } /** * This class listens to the ComboBoxModel and redraws the popup if it * must grow or shrink to accomodate the latest list of items. */ private class ListDataHandler implements ListDataListener { private int previousItemCount = -1; private final Runnable checkStrictModeInvariantRunnable = new CheckStrictModeInvariantRunnable(); public void contentsChanged(ListDataEvent e) { final int newItemCount = comboBox.getItemCount(); // if the size of the model didn't change, the popup size won't change if (previousItemCount != newItemCount) { final int maxPopupItemCount = comboBox.getMaximumRowCount(); // if the popup is showing, check if it must be resized if (popupMenu.isShowing()) { if (comboBox.isShowing()) { // if either the previous or new item count is less than the max, // hide and show the popup to recalculate its new height if (newItemCount < maxPopupItemCount || previousItemCount < maxPopupItemCount) { // don't bother unfiltering the popup since we'll redisplay the popup immediately doNotClearFilterOnPopupHide = true; try { comboBox.hidePopup(); } finally { doNotClearFilterOnPopupHide = false; } comboBox.showPopup(); } } else { // if the comboBox is not showing, simply hide the popup to avoid: // "java.awt.IllegalComponentStateException: component must be showing on the screen to determine its location" // this case can occur when the comboBox is used as a TableCellEditor // and is uninstalled (removed from the component hierarchy) before // receiving this callback comboBox.hidePopup(); } } previousItemCount = newItemCount; } // if the comboBoxModel was changed and it wasn't due to the filter changing // (i.e. !isFiltering) and it wasn't because the user selected a new // selectedItem (i.e. !userSelectedNewItem) then those changes may have // invalidated the invariant that strict mode places on the text in the // JTextField, so we must either: // // a) locate the text within the model (proving that the strict mode invariant still holds) // b) set the text to that of the first element in the model (to reestablish the invariant) final boolean userSelectedNewItem = e.getIndex0() == -1 || e.getIndex1() == -1; if (isStrict() && !userSelectedNewItem && !isFiltering) { // notice that instead of doing the work directly, we post a Runnable here // to check the strict mode invariant and repair it if it is broken. That's // important. It's necessary because we must let the current ListEvent // finish its dispatching before we attempt to change the filter of the // filteredItems list by setting new text into the comboBoxEditorComponent SwingUtilities.invokeLater(checkStrictModeInvariantRunnable); } } public void intervalAdded(ListDataEvent e) { contentsChanged(e); } public void intervalRemoved(ListDataEvent e) { contentsChanged(e); } private class CheckStrictModeInvariantRunnable implements Runnable { public void run() { final JTextField editor = comboBoxEditorComponent; if (editor != null) { final String currentText = editor.getText(); final Object item = findAutoCompleteTerm(currentText); String itemText = convertToString(item); // if we did not find the same autocomplete term if (!currentText.equals(itemText)) { // select the first item if we could not find an autocomplete term with the currentText if (item == NOT_FOUND && !allItemsUnfiltered.isEmpty()) itemText = convertToString(allItemsUnfiltered.get(0)); // set the new strict value text into the editor component editor.setText(itemText); } } } } } /** * This class sizes the popup menu of the combo box immediately before * it is shown on the screen. In particular, it will adjust the width * of the popup to accomodate a prototype display value if the combo * box contains one. */ private class PopupSizer implements PopupMenuListener { public void popupMenuWillBecomeVisible(PopupMenuEvent e) { // if the combo box does not contain a prototype display value, skip our sizing logic final Object prototypeValue = comboBox.getPrototypeDisplayValue(); if (prototypeValue == null) return; final JComponent popupComponent = (JComponent) e.getSource(); // attempt to extract the JScrollPane that scrolls the popup if (popupComponent.getComponent(0) instanceof JScrollPane) { final JScrollPane scroller = (JScrollPane) popupComponent.getComponent(0); // fetch the existing preferred size of the scroller, and we'll check if it is large enough final Dimension scrollerSize = scroller.getPreferredSize(); // calculates the preferred size of the renderer's component for the prototype value final Dimension prototypeSize = getPrototypeSize(prototypeValue); // add to the preferred width, the width of the vertical scrollbar, when it is visible prototypeSize.width += scroller.getVerticalScrollBar().getPreferredSize().width; // adjust the preferred width of the scroller, if necessary if (prototypeSize.width > scrollerSize.width) { scrollerSize.width = prototypeSize.width; // set the new size of the scroller scroller.setMaximumSize(scrollerSize); scroller.setPreferredSize(scrollerSize); scroller.setMinimumSize(scrollerSize); } } } private Dimension getPrototypeSize(Object prototypeValue) { // get the renderer responsible for drawing the prototype value ListCellRenderer renderer = comboBox.getRenderer(); if (renderer == null) renderer = new DefaultListCellRenderer(); // get the component from the renderer final Component comp = renderer.getListCellRendererComponent(popup.getList(), prototypeValue, -1, false, false); // determine the preferred size of the component comp.setFont(comboBox.getFont()); return comp.getPreferredSize(); } public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { if (doNotClearFilterOnPopupHide) return; // the popup menu is being hidden, so clear the filter to return the ComboBoxModel to its unfiltered state applyFilter(""); } public void popupMenuCanceled(PopupMenuEvent e) {} } /** * When the user selects a value from the popup with the mouse, we want to * honour their selection *without* attempting to autocomplete it to a new * term. Otherwise, it is possible that selections which are prefixes for * values that appear higher in the ComboBoxModel cannot be selected by the * mouse since they can always be successfully autocompleted to another * term. */ private class PopupMouseHandler extends MouseAdapter { @Override public void mousePressed(MouseEvent e) { doNotAutoComplete = true; } @Override public void mouseReleased(MouseEvent e) { doNotAutoComplete = false; } } /** * When the user clicks on the arrow button, we always clear the * filtering from the model to emulate Firefox style autocompletion. */ private class ArrowButtonMouseListener implements MouseListener { private final MouseListener decorated; public ArrowButtonMouseListener(MouseListener decorated) { this.decorated = decorated; } public void mousePressed(MouseEvent e) { // clear the filter if we're about to hide or show the popup // by clicking on the arrow button (this is EXPLICITLY different // than using the up/down arrow keys to show the popup) applyFilter(""); decorated.mousePressed(e); } public MouseListener getDecorated() { return decorated; } public void mouseClicked(MouseEvent e) { decorated.mouseClicked(e); } public void mouseReleased(MouseEvent e) { decorated.mouseReleased(e); } public void mouseEntered(MouseEvent e) { decorated.mouseEntered(e); } public void mouseExited(MouseEvent e) { decorated.mouseExited(e); } } /** * This KeyListener handles the case when the user hits the backspace key * and the {@link AutoCompleteSupport} is strict. Normally backspace would * delete the selected text, if it existed, or delete the character * immediately preceding the cursor. In strict mode the ComboBoxEditor must * always contain a value from the ComboBoxModel, so the backspace key * NEVER alters the Document. Rather, it alters the * text selection to include one more character to the left. This is a nice * compromise, since the editor continues to retain a valid value from the * ComboBoxModel, but the user may type a key at any point to replace the * selection with another valid entry. * * This KeyListener also makes up for a bug in normal JComboBox when * handling the enter key. Specifically, hitting enter in an stock * JComboBox that is editable produces TWO ActionEvents. * When the enter key is detected we actually unregister all * ActionListeners, process the keystroke as normal, then reregister the * listeners and broadcast an event to them, producing a single ActionEvent. */ private class AutoCompleteKeyHandler extends KeyAdapter { private ActionListener[] actionListeners; @Override public void keyPressed(KeyEvent e) { if (!isTableCellEditor) doNotTogglePopup = false; // this KeyHandler performs ALL processing of the ENTER key otherwise multiple // ActionEvents are fired to ActionListeners by the default JComboBox processing. // To control processing of the enter key, we set a flag to avoid changing the // editor's Document in any way, and also unregister the ActionListeners temporarily. if (e.getKeyChar() == KeyEvent.VK_ENTER) { doNotChangeDocument = true; this.actionListeners = unregisterAllActionListeners(comboBox); } // make sure this backspace key does not modify our comboBoxEditorComponent's Document if (isTrigger(e)) doNotChangeDocument = true; } @Override public void keyTyped(KeyEvent e) { if (isTrigger(e)) { // if no content exists in the comboBoxEditorComponent, bail early if (comboBoxEditorComponent.getText().length() == 0) return; // calculate the current beginning of the selection int selectionStart = Math.min(comboBoxEditorComponent.getSelectionStart(), comboBoxEditorComponent.getSelectionEnd()); // if we cannot extend the selection to the left, indicate the error if (selectionStart == 0) { if (getBeepOnStrictViolation()) UIManager.getLookAndFeel().provideErrorFeedback(comboBoxEditorComponent); return; } // add one character to the left of the selection selectionStart--; // select the text from the end of the Document to the new selectionStart // (which positions the caret at the selectionStart) comboBoxEditorComponent.setCaretPosition(comboBoxEditorComponent.getText().length()); comboBoxEditorComponent.moveCaretPosition(selectionStart); } } @Override public void keyReleased(KeyEvent e) { // resume the ability to modify our comboBoxEditorComponent's Document if (isTrigger(e)) doNotChangeDocument = false; // keyPressed(e) has disabled the JComboBox's normal processing of the enter key // so now it is time to perform our own processing. We reattach all ActionListeners // and simulate exactly ONE ActionEvent in the JComboBox and then reenable Document changes. if (e.getKeyChar() == KeyEvent.VK_ENTER) { updateFilter(); // reregister all ActionListeners and then notify them due to the ENTER key // Note: We *must* check for a null ActionListener[]. The reason // is that it is possible to receive a keyReleased() callback // *without* a corresponding keyPressed() callback! It occurs // when focus is transferred away from the ComboBoxEditor and // then the ENTER key transfers focus back to the ComboBoxEditor. if (actionListeners != null) { registerAllActionListeners(comboBox, actionListeners); comboBox.actionPerformed(new ActionEvent(e.getSource(), e.getID(), null)); } // null out our own reference to the ActionListeners actionListeners = null; // reenable Document changes once more doNotChangeDocument = false; } if (!isTableCellEditor) doNotTogglePopup = true; } private boolean isTrigger(KeyEvent e) { return isStrict() && e.getKeyChar() == KeyEvent.VK_BACK_SPACE; } } /** * To emulate Firefox behaviour, all text in the ComboBoxEditor is selected * from beginning to end when the ComboBoxEditor gains focus if the value * returned from {@link AutoCompleteSupport#getSelectsTextOnFocusGain()} * allows this behaviour. In addition, the JPopupMenu is hidden when the * ComboBoxEditor loses focus if the value returned from * {@link AutoCompleteSupport#getHidesPopupOnFocusLost()} allows this * behaviour. */ private class ComboBoxEditorFocusHandler extends FocusAdapter { @Override public void focusGained(FocusEvent e) { if (getSelectsTextOnFocusGain()) comboBoxEditorComponent.select(0, comboBoxEditorComponent.getText().length()); } @Override public void focusLost(FocusEvent e) { if (comboBox.isPopupVisible() && getHidesPopupOnFocusLost()) comboBox.setPopupVisible(false); } } /** * Watch for a change of the ComboBoxUI and reinstall the necessary * behaviour customizations. */ private class UIWatcher implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent evt) { undecorateOriginalUI(); decorateCurrentUI(); } } /** * Watch for a change of the ComboBoxModel and report it as a violation. */ private class ModelWatcher implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent evt) { throwIllegalStateException("The ComboBoxModel cannot be changed. It was changed to: " + evt.getNewValue()); } } /** * Watch the Document behind the editor component in case it changes. If a * new Document is swapped in, uninstall our DocumentFilter from the old * Document and install it on the new. */ private class DocumentWatcher implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent evt) { final Document newDocument = (Document) evt.getNewValue(); if (!(newDocument instanceof AbstractDocument)) throwIllegalStateException("The Document behind the JTextField was changed to no longer be an AbstractDocument. It was changed to: " + newDocument); // remove our DocumentFilter from the old document document.setDocumentFilter(null); // update the document we track internally document = (AbstractDocument) newDocument; // add our DocumentFilter to the new Document document.setDocumentFilter(documentFilter); } } /** * A custom renderer which honours the custom Format given by the user when * they invoked the install method. */ private class StringFunctionRenderer extends DefaultListCellRenderer { @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { String string = convertToString(value); // JLabels require some text before they can correctly determine their height, so we convert "" to " " if (string.length() == 0) string = " "; return super.getListCellRendererComponent(list, string, index, isSelected, cellHasFocus); } } /** * A decorated version of the ComboBoxEditor that does NOT assume that * Object.toString() is the proper way to convert values from the * ComboBoxModel into Strings for the ComboBoxEditor's component. It uses * convertToString(E) instead. * * We implement the UIResource interface here so that changes in the UI * delegate of the JComboBox will *replace* this ComboBoxEditor with one * that is correct for the new L&F. We will then react to the change of UI * delegate by installing a new FormatComboBoxEditor overtop of the * UI Delegate's default ComboBoxEditor. */ private class FormatComboBoxEditor implements ComboBoxEditor, UIResource { /** This is the ComboBoxEditor installed by the current UI Delegate of the JComboBox. */ private final ComboBoxEditor delegate; private Object oldValue; public FormatComboBoxEditor(ComboBoxEditor delegate) { this.delegate = delegate; } public ComboBoxEditor getDelegate() { return delegate; } /** * BasicComboBoxEditor defines this method to call: * * editor.setText(anObject.toString()); * * we intercept and replace it with our own String conversion logic * to remain consistent throughout. */ public void setItem(Object anObject) { oldValue = anObject; ((JTextField) getEditorComponent()).setText(convertToString(anObject)); } /** * BasicComboBoxEditor defines this method to use reflection to try * finding a method called valueOf(String) in order to return the * item. We attempt to find a user-supplied Format before * resorting to the valueOf(String) call. */ public Object getItem() { final String oldValueString = convertToString(oldValue); final String currentString = ((JTextField) getEditorComponent()).getText(); // if the String value in the editor matches the String version of // the last item that was set in the editor, return the item if (GlazedListsImpl.equal(oldValueString, currentString)) return oldValue; // if the user specified a Format, use it if (format != null) return format.parseObject(currentString, PARSE_POSITION); // otherwise, use the default algorithm from BasicComboBoxEditor to produce a value if (oldValue != null && !(oldValue instanceof String)) { try { final Method method = oldValue.getClass().getMethod("valueOf", VALUE_OF_SIGNATURE); return method.invoke(oldValue, new Object[] {currentString}); } catch (Exception ex) { // fail silently and return the current string } } return currentString; } public Component getEditorComponent() { return delegate.getEditorComponent(); } public void selectAll() { delegate.selectAll(); } public void addActionListener(ActionListener l) { delegate.addActionListener(l); } public void removeActionListener(ActionListener l) { delegate.removeActionListener(l); } } /** * This default implementation of the TextFilterator interface uses the * same strategy for producing Strings from ComboBoxModel objects as the * renderer and editor. */ class DefaultTextFilterator implements TextFilterator { public void getFilterStrings(List baseList, E element) { baseList.add(convertToString(element)); } } /** * This extension of DefaultCellEditor exists solely to provide a handle to * the AutoCompleteSupport object that is providing autocompletion * capabilities to the JComboBox. */ public static class AutoCompleteCellEditor extends DefaultCellEditor { private final AutoCompleteSupport autoCompleteSupport; /** * Construct a TableCellEditor using the JComboBox supplied by the * given autoCompleteSupport. Specifically, the JComboBox * is retrieved using {@link AutoCompleteSupport#getComboBox()}. */ public AutoCompleteCellEditor(AutoCompleteSupport autoCompleteSupport) { super(autoCompleteSupport.getComboBox()); this.autoCompleteSupport = autoCompleteSupport; } /** * Returns the AutoCompleteSupport object that controls the * autocompletion behaviour for the JComboBox. */ public AutoCompleteSupport getAutoCompleteSupport() { return autoCompleteSupport; } } /** * This factory method creates and returns a {@link AutoCompleteCellEditor} * which adapts an autocompleting {@link JComboBox} for use as a Table * Cell Editor. The values within the table column are used as * autocompletion terms within the {@link ComboBoxModel}. * *

This version of createTableCellEditor assumes that the * values stored in the TableModel at the given columnIndex * are all {@link Comparable}, and that the natural ordering defined by * those {@link Comparable} values also determines which are duplicates * (and thus can safely be removed) and which are unique (and thus must * remain in the {@link ComboBoxModel}). * *

Note that this factory method is only appropriate for use when the * values in the {@link ComboBoxModel} should be the unique set of values * in a table column. If some other list of values will be used then * {@link #createTableCellEditor(EventList)} is the appropriate factory * method to use. * *

If the appearance or function of the autocompleting {@link JComboBox} * is to be customized, it can be retrieved using * {@link AutoCompleteCellEditor#getComponent()}. * * @param tableFormat specifies how each row object within a table is * broken apart into column values * @param tableData the {@link EventList} backing the TableModel * @param columnIndex the index of the column for which to return a * {@link AutoCompleteCellEditor} * @return a {@link AutoCompleteCellEditor} which contains an autocompleting * combobox whose contents remain consistent with the data in the * table column at the given columnIndex */ public static AutoCompleteCellEditor createTableCellEditor(TableFormat tableFormat, EventList tableData, int columnIndex) { return createTableCellEditor(GlazedLists.comparableComparator(), tableFormat, tableData, columnIndex); } /** * This factory method creates and returns a {@link AutoCompleteCellEditor} * which adapts an autocompleting {@link JComboBox} for use as a Table * Cell Editor. The values within the table column are used as * autocompletion terms within the {@link ComboBoxModel}. * *

This version of createTableCellEditor makes no * assumption about the values stored in the TableModel at the given * columnIndex. Instead, it uses the given * uniqueComparator to determine which values are duplicates * (and thus can safely be removed) and which are unique (and thus must * remain in the {@link ComboBoxModel}). * *

Note that this factory method is only appropriate for use when the * values in the {@link ComboBoxModel} should be the unique set of values * in a table column. If some other list of values will be used then * {@link #createTableCellEditor(EventList)} is the appropriate factory * method to use. * *

If the appearance or function of the autocompleting {@link JComboBox} * is to be customized, it can be retrieved using * {@link AutoCompleteCellEditor#getComponent()}. * * @param uniqueComparator the {@link Comparator} that strips away * duplicate elements from the {@link ComboBoxModel} * @param tableFormat specifies how each row object within a table is * broken apart into column values * @param tableData the {@link EventList} backing the TableModel * @param columnIndex the index of the column for which to return a * {@link AutoCompleteCellEditor} * @return a {@link AutoCompleteCellEditor} which contains an autocompleting * combobox whose contents remain consistent with the data in the * table column at the given columnIndex */ public static AutoCompleteCellEditor createTableCellEditor(Comparator uniqueComparator, TableFormat tableFormat, EventList tableData, int columnIndex) { // use a function to extract all values for the column final FunctionList.Function columnValueFunction = new TableColumnValueFunction(tableFormat, columnIndex); final FunctionList allColumnValues = new FunctionList(tableData, columnValueFunction); // narrow the list to just unique values within the column final EventList uniqueColumnValues = new UniqueList(allColumnValues, uniqueComparator); return createTableCellEditor(uniqueColumnValues); } /** * This factory method creates and returns a {@link AutoCompleteCellEditor} * which adapts an autocompleting {@link JComboBox} for use as a Table * Cell Editor. The values within the source are used as * autocompletion terms within the {@link ComboBoxModel}. * *

If the appearance or function of the autocompleting {@link JComboBox} * is to be customized, it can be retrieved using * {@link AutoCompleteCellEditor#getComponent()}. * * @param source the source of data for the JComboBox within the table cell editor * @return a {@link AutoCompleteCellEditor} which contains an autocompleting * combobox whose model contents are determined by the given source */ public static AutoCompleteCellEditor createTableCellEditor(EventList source) { // build a special JComboBox used only in Table Cell Editors final JComboBox comboBox = new TableCellComboBox(); comboBox.putClientProperty("JComboBox.isTableCellEditor", Boolean.TRUE); // install autocompletion support on the special JComboBox final AutoCompleteSupport autoCompleteSupport = AutoCompleteSupport.install(comboBox, source); autoCompleteSupport.setSelectsTextOnFocusGain(false); // create an AutoCompleteCellEditor using the AutoCompleteSupport object final AutoCompleteCellEditor cellEditor = new AutoCompleteCellEditor(autoCompleteSupport); cellEditor.setClickCountToStart(2); return cellEditor; } /** * This customized JComboBox is only used when creating an autocompleting * {@link DefaultCellEditor}. It customizes the behaviour of a JComboBox to * make it more appropriate for use as a TableCellEditor. Specifically it * adds the following: * *

    *
  • key presses which start table cell edits are also respected by the * JTextField *
  • the next focusable component for the JTextField is set to be the * JTable when editing begins so that focus returns to the table when * editing stops *
*/ private static final class TableCellComboBox extends JComboBox implements FocusListener { public TableCellComboBox() { // use a customized ComboBoxEditor within this special JComboBox setEditor(new TableCellComboBoxEditor()); // replace the UI Delegate's FocusListener with our own in both // the JComboBox and its ComboBoxEditor replaceUIDelegateFocusListener(getEditor().getEditorComponent(), this); replaceUIDelegateFocusListener(this, this); } /** * This method is a complete hack, but is necessary to achieve the * desired behaviour when using an autocompleting JComboBox in a * TableCellEditor. * * The problem is that when cell editing begins due to a keystroke, * ideally the ComboBoxPopup should be displayed in a filtered state. * But, the FocusListener installed by BasicComboBoxUI actually hides * the ComboBoxPopup due to some phantom focusLost event we receive. * * To solve the problem, we rip out the FocusListener installed by * the BasicComboBoxUI and replace it with our own that does NOT hide * the popup when this JComboBox loses focus. That's with us since * losing focus implies we are committing or cancelling the cell edit * anyway, so the entire editor is about to be removed. */ private static void replaceUIDelegateFocusListener(Component c, FocusListener replacement) { // remove all FocusListeners that appear to be installed by the UIdelegate final FocusListener[] focusListeners = c.getFocusListeners(); for (int i = 0; i < focusListeners.length; i++) if (focusListeners[i].getClass().getName().indexOf("ComboBoxUI") != -1) c.removeFocusListener(focusListeners[i]); c.addFocusListener(replacement); } /** * Repaint and request focus if editable. */ public void focusGained(FocusEvent e) { final ComboBoxEditor currentEditor = getEditor(); if (currentEditor != null && currentEditor.getEditorComponent() != e.getSource()) { repaint(); if (isEditable()) { currentEditor.getEditorComponent().requestFocus(); } } } /** * BasicComboBoxUI.Handler.focusLost screws up the installation of this * JComboBox as a TableCellEditor by hiding the ComboBoxPopup on the * first keystroke and represent the reason why we must tear out the * FocusListener and replace it with one of our own. */ public void focusLost(FocusEvent e) { final ComboBoxEditor currentEditor = getEditor(); if (!e.isTemporary() && currentEditor != null && currentEditor.getEditorComponent() == e.getSource()) { final Object currentItem = currentEditor.getItem(); if (currentItem != null && !currentItem.equals(getSelectedItem())) { fireActionPerformed(currentEditor); } } repaint(); } private void fireActionPerformed(ComboBoxEditor source) { actionPerformed(new ActionEvent(source, 0, "", EventQueue.getMostRecentEventTime(), 0)); } /** * This method is called by Swing when the JComboBox is installed as a * TableCellEditor. It gives the component a chance to process the * KeyEvent. For example, a JTextField will honour the keystroke and * add the letter to its Document. * * Editable JComboBoxes don't provide that expected behaviour out of * the box, so we override this method with logic that gives the editor * component of the JComboBox a chance to respond to the keystroke that * initiated the cell edit. */ @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) { // let the textfield have a crack at processing the KeyEvent final TableCellTextField tableCellTextField = (TableCellTextField) getEditor().getEditorComponent(); tableCellTextField.processKeyBinding(ks, e, condition, pressed); // ensure the text field has focus if it is still processing key strokes // (I've seen bad behaviour on windows textfield has no cursor, yet continues to process keystrokes // - this helps to ensure that the textfield actually has focus and thus the cursor) if (!tableCellTextField.hasFocus()) tableCellTextField.requestFocus(); // now let the JComboBox react (important for arrow keys to work as expected) return super.processKeyBinding(ks, e, condition, pressed); } /** * This method is called by Swing when installing this JComboBox as a * TableCellEditor. It ensures that focus will return to the JTable * when the cell edit is complete. * *

We override this method to ensure that if the JTextField acting * as the editor of the JComboBox has focus when the cell edit is * complete, focus is returned to the JTable in that case as well. */ @Override public void setNextFocusableComponent(Component aComponent) { super.setNextFocusableComponent(aComponent); // set the next focusable component for the editor as well ((JComponent) getEditor().getEditorComponent()).setNextFocusableComponent(aComponent); } /** * A custom BasicComboBoxEditor that builds a custom JTextField with * an extra capability: a public implementation of * {@link TableCellTextField#processKeyBinding} */ private static final class TableCellComboBoxEditor extends BasicComboBoxEditor { public TableCellComboBoxEditor() { // replace the super's editor with a JTextField of our own design editor = new TableCellTextField(); } } /** * This custom JTextField exists solely to make * {@link #processKeyBinding} a public method so that it can be called * from {@link TableCellComboBox#processKeyBinding}. * * This custom JTextField is only used when creating an autocompleting * TableCellEditor via {@link AutoCompleteSupport#createTableCellEditor}. */ private static class TableCellTextField extends JTextField { public TableCellTextField() { super("", 9); } /** * {@inheritDoc} */ @Override public void setText(String newText) { // workaround for bug 4530952 if (!equalsText(newText)) { super.setText(newText); } } private boolean equalsText(String newText) { final String currentText = getText(); return (currentText == null) ? newText == null : currentText.equals(newText); } /** * {@inheritDoc} */ @Override public void setBorder(Border b) { // NOP, we want no border } /** * We override this method to make it public so that it can be * called from {@link TableCellComboBox#processKeyBinding}. * *

This allows the keystroke which begins a table cell edit to * also contribute a character to this JTextField, thus mimicing * the behaviour of normal editable JTextField table cell editors. */ @Override public boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) { return super.processKeyBinding(ks, e, condition, pressed); } } } /** * This function uses a TableFormat and columnIndex to extract all of the * values that are displayed in the given table column. These values are * used as autocompletion terms when editing a cell within that column. */ private static final class TableColumnValueFunction implements FunctionList.Function { private final TableFormat tableFormat; private final int columnIndex; public TableColumnValueFunction(TableFormat tableFormat, int columnIndex) { this.tableFormat = tableFormat; this.columnIndex = columnIndex; } public Object evaluate(E sourceValue) { return tableFormat.getColumnValue(sourceValue, columnIndex); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy