ca.odell.glazedlists.swing.AutoCompleteSupport Maven / Gradle / Ivy
Show all versions of glazedlists_java15 Show documentation
/* 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
*
* - 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.
* - 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.
* - 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.
* - 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.
* - typing the up arrow key when no selection exists causes the last
* element of the popup to become selected and used for autocompletion
* - 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
* - typing the down arrow key when no selection exists causes the first
* element of the popup to become selected and used for autocompletion
* - 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
*
* - clicking the arrow button when the popup is invisible causes the
* popup to appear and its contents to be shown unfiltered
*
- clicking the arrow button when the popup is visible causes the popup
* to be hidden
*
*
* Sizing the ComboBox Popup
*
* - 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
*
- 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:
*
* - the user hits the enter key
*
- 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)
*
- 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
*
* - the text in the ComboBoxEditor is selected if
* {@link #getSelectsTextOnFocusGain()} returns true
*
- 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 super E> 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 super E> 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 super E> 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 super E> 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:
*
*
* - save the prefix as the user has entered it
*
- filter the combo box items against the prefix
*
- update the text in the combo box editor with an autocomplete suggestion
*
- 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);
}
}
}