| /******************************************************************************* |
| * Copyright (c) 2007, 2008 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.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.utility.internal.SimpleStringMatcher; |
| import org.eclipse.jpt.utility.internal.StringConverter; |
| import org.eclipse.jpt.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"; |
| } |
| }; |
| } |
| |
| 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 <=========="; |
| } |
| |
| /** |
| * 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]; |
| } |
| }; |
| } |
| } |