blob: e58608c9e4d0e651ea3ea1b828fae76139253335 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2010 Oracle. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0, which accompanies this distribution
* and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Contributors:
* Oracle - initial API and implementation
******************************************************************************/
package org.eclipse.jpt.common.utility.internal.swing;
import java.awt.BorderLayout;
import java.awt.Font;
import javax.swing.AbstractListModel;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.border.Border;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.eclipse.jpt.common.utility.internal.SimpleStringMatcher;
import org.eclipse.jpt.common.utility.internal.StringConverter;
import org.eclipse.jpt.common.utility.internal.StringMatcher;
/**
* This panel presents an entry field and a list box of choices that
* allows the user to filter the entries in the list box by entering
* a pattern in the entry field.
*
* By default, two wildcards are allowed in the pattern:
* '*' will match any set of zero or more characters
* '?' will match any single character
*
* The panel consists of 4 components that can be customized:
* - 1 text field
* - 1 list box
* - 2 labels, one for each of the above
*
* Other aspects of the panel's behavior can be changed:
* - the string converter determines how the objects in the
* list are converted to strings and compared to the pattern
* entered in the text field; by default the converter simply
* uses the result of the object's #toString() method
* (if you replace the string converter, you will probably
* want to replace the list box's cell renderer also)
* - the string matcher can also be changed if you would
* like different pattern matching behavior than that
* described above
* - you can specify the maximum size of the list - this may
* force the user to enter a pattern restrictive enough
* to result in a list smaller than the maximum size; the
* default is -1, which disables the restriction
*
* This panel is not a typical panel, in the sense that it does not share
* its model with clients via value models. Instead, this panel's model
* is set and queried directly because it is designed to be used in a
* dialog that directs the user's behavior (as opposed to a "normal"
* window).
*/
public class FilteringListPanel<T> extends JPanel {
/**
* The complete list of available choices
* (as opposed to the partial list held by the list box).
*/
private Object[] completeList;
/**
* An adapter used to convert the objects in the list
* to strings so they can be run through the matcher
* and displayed in the text field.
*/
StringConverter<T> stringConverter;
/** The text field. */
private JTextField textField;
private JLabel textFieldLabel;
private DocumentListener textFieldListener;
/** The list box. */
private JList listBox;
private JLabel listBoxLabel;
/** The maximum number of entries displayed in the list box. */
private int maxListSize;
/**
* The matcher used to filter the list against
* the pattern entered in the text field. By default,
* this allows the two wildcard characters described in
* the class comment.
*/
private StringMatcher stringMatcher;
/**
* Performance tweak: We use this buffer instead of
* a temporary variable during filtering so we don't have
* to keep re-allocating it.
*/
private Object[] buffer;
private static final Border TEXT_FIELD_LABEL_BORDER = BorderFactory.createEmptyBorder(0, 0, 5, 0);
private static final Border LIST_BOX_LABEL_BORDER = BorderFactory.createEmptyBorder(5, 0, 5, 0);
// ********** constructors **********
/**
* Construct a FilteringListPanel with the specified list of choices
* and initial selection. Use the default string converter to convert the
* choices and selection to strings (which simply calls #toString() on
* the objects).
*/
public FilteringListPanel(Object[] completeList, Object initialSelection) {
this(completeList, initialSelection, StringConverter.Default.<T>instance());
}
/**
* Construct a FilteringListPanel with the specified list of choices
* and initial selection. Use the specified string converter to convert the
* choices and selection to strings.
*/
public FilteringListPanel(Object[] completeList, Object initialSelection, StringConverter<T> stringConverter) {
super(new BorderLayout());
this.completeList = completeList;
this.stringConverter = stringConverter;
this.initialize(initialSelection);
}
// ********** initialization **********
private void initialize(Object initialSelection) {
this.maxListSize = this.defaultMaxListSize();
this.buffer = this.buildBuffer();
this.textFieldListener = this.buildTextFieldListener();
this.stringMatcher = this.buildStringMatcher();
this.initializeLayout(initialSelection);
}
private Object[] buildBuffer() {
return new Object[this.max()];
}
/**
* Return the current max number of entries allowed in the list box.
*/
private int max() {
if (this.maxListSize == -1) {
return this.completeList.length;
}
return Math.min(this.maxListSize, this.completeList.length);
}
/**
* Build a listener that will listen to changes in the text field
* and filter the list appropriately.
*/
private DocumentListener buildTextFieldListener() {
return new DocumentListener() {
public void insertUpdate(DocumentEvent e) {
FilteringListPanel.this.filterList();
}
public void changedUpdate(DocumentEvent e) {
FilteringListPanel.this.filterList();
}
public void removeUpdate(DocumentEvent e) {
FilteringListPanel.this.filterList();
}
@Override
public String toString() {
return "text field listener"; //$NON-NLS-1$
}
};
}
private int defaultMaxListSize() {
return -1;
}
private StringMatcher buildStringMatcher() {
return new SimpleStringMatcher<T>();
}
private void initializeLayout(Object initialSelection) {
// text field
JPanel textFieldPanel = new JPanel(new BorderLayout());
this.textFieldLabel = new JLabel();
this.textFieldLabel.setBorder(TEXT_FIELD_LABEL_BORDER);
textFieldPanel.add(this.textFieldLabel, BorderLayout.NORTH);
this.textField = new JTextField();
this.textField.getDocument().addDocumentListener(this.textFieldListener);
this.textFieldLabel.setLabelFor(this.textField);
textFieldPanel.add(this.textField, BorderLayout.CENTER);
this.add(textFieldPanel, BorderLayout.NORTH);
// list box
JPanel listBoxPanel = new JPanel(new BorderLayout());
this.listBoxLabel = new JLabel();
this.listBoxLabel.setBorder(LIST_BOX_LABEL_BORDER);
listBoxPanel.add(this.listBoxLabel, BorderLayout.NORTH);
this.listBox = new JList();
this.listBox.setDoubleBuffered(true);
this.listBox.setModel(this.buildPartialArrayListModel(this.completeList, this.max()));
this.listBox.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// performance tweak(?)
this.listBox.setPrototypeCellValue(this.prototypeCellValue());
this.listBox.setPrototypeCellValue(null);
this.listBox.setCellRenderer(this.buildDefaultCellRenderer());
this.listBoxLabel.setLabelFor(this.listBox);
// bug 2777802 - scroll bars shouldn't be on the tab sequence
JScrollPane listBoxScrollPane = new JScrollPane(this.listBox);
listBoxScrollPane.getHorizontalScrollBar().setFocusable(false);
listBoxScrollPane.getVerticalScrollBar().setFocusable(false);
listBoxPanel.add(listBoxScrollPane, BorderLayout.CENTER);
// initialize the widgets
this.listBox.setSelectedValue(initialSelection, true);
this.textField.select(0, this.textField.getText().length());
this.add(listBoxPanel, BorderLayout.CENTER);
}
// ********** public API **********
public Object selection() {
return this.listBox.getSelectedValue();
}
public void setSelection(Object selection) {
this.listBox.setSelectedValue(selection, true);
}
public Object[] completeList() {
return this.completeList;
}
/**
* rebuild the filtering buffer and re-apply the filter
* to the new list
*/
public void setCompleteList(Object[] completeList) {
this.completeList = completeList;
if (this.buffer.length < this.max()) {
// the buffer will never shrink - might want to re-consider... ~bjv
this.buffer = this.buildBuffer();
}
this.filterList();
}
public int maxListSize() {
return this.maxListSize;
}
public void setMaxListSize(int maxListSize) {
this.maxListSize = maxListSize;
if (this.buffer.length < this.max()) {
// the buffer will never shrink - might want to re-consider... ~bjv
this.buffer = this.buildBuffer();
}
this.filterList();
}
public StringConverter<T> stringConverter() {
return this.stringConverter;
}
/**
* apply the new filter to the list
*/
public void setStringConverter(StringConverter<T> stringConverter) {
this.stringConverter = stringConverter;
this.filterList();
}
/**
* allow client code to access the text field
* (so we can set the focus)
*/
public JTextField textField() {
return this.textField;
}
/**
* allow client code to access the text field label
*/
public JLabel textFieldLabel() {
return this.textFieldLabel;
}
/**
* convenience method
*/
public void setTextFieldLabelText(String text) {
this.textFieldLabel.setText(text);
}
/**
* allow client code to access the list box
* (so we can add mouse listeners for double-clicking)
*/
public JList listBox() {
return this.listBox;
}
/**
* convenience method
*/
public void setListBoxCellRenderer(ListCellRenderer renderer) {
this.listBox.setCellRenderer(renderer);
}
/**
* allow client code to access the list box label
*/
public JLabel listBoxLabel() {
return this.listBoxLabel;
}
/**
* convenience method
*/
public void setListBoxLabelText(String text) {
this.listBoxLabel.setText(text);
}
/**
* convenience method
*/
public void setComponentsFont(Font font) {
this.textFieldLabel.setFont(font);
this.textField.setFont(font);
this.listBoxLabel.setFont(font);
this.listBox.setFont(font);
}
public StringMatcher stringMatcher() {
return this.stringMatcher;
}
/**
* re-apply the filter to the list
*/
public void setStringMatcher(StringMatcher stringMatcher) {
this.stringMatcher = stringMatcher;
this.filterList();
}
// ********** internal methods **********
/**
* Allow subclasses to disable performance tweak
* by returning null here.
*/
protected String prototypeCellValue() {
return "==========> A_STRING_THAT_IS_DEFINITELY_LONGER_THAN_EVERY_STRING_IN_THE_LIST <=========="; //$NON-NLS-1$
}
/**
* By default, use the string converter to build the text
* used by the list box's cell renderer.
*/
protected ListCellRenderer buildDefaultCellRenderer() {
return new SimpleListCellRenderer() {
@Override
@SuppressWarnings("unchecked")
protected String buildText(Object value) {
return FilteringListPanel.this.stringConverter.convertToString((T) value);
}
};
}
/**
* Something has changed that requires us to filter the list.
*
* This method is synchronized because a fast typist can
* generate events quicker than we can filter the list. (? ~bjv)
*/
synchronized void filterList() {
// temporarily stop listening to the list box selection, since we will
// be changing the selection during the filtering and don't want
// that to affect the text field
this.filterList(this.textField.getText());
}
/**
* Filter the contents of the list box to match the
* specified pattern.
*/
private void filterList(String pattern) {
if (pattern.length() == 0) {
this.listBox.setModel(this.buildPartialArrayListModel(this.completeList, this.max()));
} else {
this.stringMatcher.setPatternString(pattern);
int j = 0;
int len = this.completeList.length;
int max = this.max();
for (int i = 0; i < len; i++) {
if (this.stringMatcher.matches(this.stringConverter.convertToString(this.entry(i)))) {
this.buffer[j++] = this.completeList[i];
}
if (j == max) {
break;
}
}
this.listBox.setModel(this.buildPartialArrayListModel(this.buffer, j));
}
// after filtering the list, determine the appropriate selection
if (this.listBox.getModel().getSize() == 0) {
this.listBox.getSelectionModel().clearSelection();
} else {
this.listBox.getSelectionModel().setAnchorSelectionIndex(0);
this.listBox.getSelectionModel().setLeadSelectionIndex(0);
this.listBox.ensureIndexIsVisible(0);
}
}
/**
* minimize scope of suppressed warnings
*/
@SuppressWarnings("unchecked")
private T entry(int index) {
return (T) this.completeList[index];
}
/**
* Build a list model that wraps only a portion of the specified array.
* The model will include the array entries from 0 to (size - 1).
*/
private ListModel buildPartialArrayListModel(final Object[] array, final int size) {
return new AbstractListModel() {
public int getSize() {
return size;
}
public Object getElementAt(int index) {
return array[index];
}
};
}
}