--- /dev/null
+package net.sourceforge.phpdt.internal.ui.util;
+
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.Vector;
+
+import org.eclipse.jface.util.Assert;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+
+/**
+ * A composite widget which holds a list of elements for user selection. The
+ * elements are sorted alphabetically. Optionally, the elements can be filtered
+ * and duplicate entries can be hidden (folding).
+ */
+public class FilteredList extends Composite {
+
+ public interface FilterMatcher {
+ /**
+ * Sets the filter.
+ *
+ * @param pattern
+ * the filter pattern.
+ * @param ignoreCase
+ * a flag indicating whether pattern matching is case
+ * insensitive or not.
+ * @param ignoreWildCards
+ * a flag indicating whether wildcard characters are
+ * interpreted or not.
+ */
+ void setFilter(String pattern, boolean ignoreCase,
+ boolean ignoreWildCards);
+
+ /**
+ * Returns <code>true</code> if the object matches the pattern,
+ * <code>false</code> otherwise. <code>setFilter()</code> must have
+ * been called at least once prior to a call to this method.
+ */
+ boolean match(Object element);
+ }
+
+ private class DefaultFilterMatcher implements FilterMatcher {
+ private StringMatcher fMatcher;
+
+ public void setFilter(String pattern, boolean ignoreCase,
+ boolean ignoreWildCards) {
+ fMatcher = new StringMatcher(pattern + '*', ignoreCase,
+ ignoreWildCards);
+ }
+
+ public boolean match(Object element) {
+ return fMatcher.match(fRenderer.getText(element));
+ }
+ }
+
+ private Table fList;
+
+ private ILabelProvider fRenderer;
+
+ private boolean fMatchEmtpyString = true;
+
+ private boolean fIgnoreCase;
+
+ private boolean fAllowDuplicates;
+
+ private String fFilter = ""; //$NON-NLS-1$
+
+ private TwoArrayQuickSorter fSorter;
+
+ private Object[] fElements = new Object[0];
+
+ private Label[] fLabels;
+
+ private Vector fImages = new Vector();
+
+ private int[] fFoldedIndices;
+
+ private int fFoldedCount;
+
+ private int[] fFilteredIndices;
+
+ private int fFilteredCount;
+
+ private FilterMatcher fFilterMatcher = new DefaultFilterMatcher();
+
+ private Comparator fComparator;
+
+ private static class Label {
+ public final String string;
+
+ public final Image image;
+
+ public Label(String string, Image image) {
+ this.string = string;
+ this.image = image;
+ }
+
+ public boolean equals(Label label) {
+ if (label == null)
+ return false;
+
+ return string.equals(label.string) && image.equals(label.image);
+ }
+ }
+
+ private final class LabelComparator implements Comparator {
+ private boolean fIgnoreCase;
+
+ LabelComparator(boolean ignoreCase) {
+ fIgnoreCase = ignoreCase;
+ }
+
+ public int compare(Object left, Object right) {
+ Label leftLabel = (Label) left;
+ Label rightLabel = (Label) right;
+
+ int value;
+
+ if (fComparator == null) {
+ value = fIgnoreCase ? leftLabel.string
+ .compareToIgnoreCase(rightLabel.string)
+ : leftLabel.string.compareTo(rightLabel.string);
+ } else {
+ value = fComparator
+ .compare(leftLabel.string, rightLabel.string);
+ }
+
+ if (value != 0)
+ return value;
+
+ // images are allowed to be null
+ if (leftLabel.image == null) {
+ return (rightLabel.image == null) ? 0 : -1;
+ } else if (rightLabel.image == null) {
+ return +1;
+ } else {
+ return fImages.indexOf(leftLabel.image)
+ - fImages.indexOf(rightLabel.image);
+ }
+ }
+
+ }
+
+ /**
+ * Constructs a new instance of a filtered list.
+ *
+ * @param parent
+ * the parent composite.
+ * @param style
+ * the widget style.
+ * @param renderer
+ * the label renderer.
+ * @param ignoreCase
+ * specifies whether sorting and folding is case sensitive.
+ * @param allowDuplicates
+ * specifies whether folding of duplicates is desired.
+ * @param matchEmptyString
+ * specifies whether empty filter strings should filter
+ * everything or nothing.
+ */
+ public FilteredList(Composite parent, int style, ILabelProvider renderer,
+ boolean ignoreCase, boolean allowDuplicates,
+ boolean matchEmptyString) {
+ super(parent, SWT.NONE);
+
+ GridLayout layout = new GridLayout();
+ layout.marginHeight = 0;
+ layout.marginWidth = 0;
+ setLayout(layout);
+
+ fList = new Table(this, style);
+ fList.setLayoutData(new GridData(GridData.FILL_BOTH));
+ fList.addDisposeListener(new DisposeListener() {
+ public void widgetDisposed(DisposeEvent e) {
+ fRenderer.dispose();
+ }
+ });
+
+ fRenderer = renderer;
+ fIgnoreCase = ignoreCase;
+ fSorter = new TwoArrayQuickSorter(new LabelComparator(ignoreCase));
+ fAllowDuplicates = allowDuplicates;
+ fMatchEmtpyString = matchEmptyString;
+ }
+
+ /**
+ * Sets the list of elements.
+ *
+ * @param elements
+ * the elements to be shown in the list.
+ */
+ public void setElements(Object[] elements) {
+ if (elements == null) {
+ fElements = new Object[0];
+ } else {
+ // copy list for sorting
+ fElements = new Object[elements.length];
+ System.arraycopy(elements, 0, fElements, 0, elements.length);
+ }
+
+ int length = fElements.length;
+
+ // fill labels
+ fLabels = new Label[length];
+ Set imageSet = new HashSet();
+ for (int i = 0; i != length; i++) {
+ String text = fRenderer.getText(fElements[i]);
+ Image image = fRenderer.getImage(fElements[i]);
+
+ fLabels[i] = new Label(text, image);
+ imageSet.add(image);
+ }
+ fImages.clear();
+ fImages.addAll(imageSet);
+
+ fSorter.sort(fLabels, fElements);
+
+ fFilteredIndices = new int[length];
+ fFilteredCount = filter();
+
+ fFoldedIndices = new int[length];
+ fFoldedCount = fold();
+
+ updateList();
+ }
+
+ /**
+ * Tests if the list (before folding and filtering) is empty.
+ *
+ * @return returns <code>true</code> if the list is empty,
+ * <code>false</code> otherwise.
+ */
+ public boolean isEmpty() {
+ return (fElements == null) || (fElements.length == 0);
+ }
+
+ /**
+ * Sets the filter matcher.
+ */
+ public void setFilterMatcher(FilterMatcher filterMatcher) {
+ Assert.isNotNull(filterMatcher);
+ fFilterMatcher = filterMatcher;
+ }
+
+ /**
+ * Sets a custom comparator for sorting the list.
+ */
+ public void setComparator(Comparator comparator) {
+ Assert.isNotNull(comparator);
+ fComparator = comparator;
+ }
+
+ /**
+ * Adds a selection listener to the list.
+ *
+ * @param listener
+ * the selection listener to be added.
+ */
+ public void addSelectionListener(SelectionListener listener) {
+ fList.addSelectionListener(listener);
+ }
+
+ /**
+ * Removes a selection listener from the list.
+ *
+ * @param listener
+ * the selection listener to be removed.
+ */
+ public void removeSelectionListener(SelectionListener listener) {
+ fList.removeSelectionListener(listener);
+ }
+
+ /**
+ * Sets the selection of the list.
+ *
+ * @param selection
+ * an array of indices specifying the selection.
+ */
+ public void setSelection(int[] selection) {
+ fList.setSelection(selection);
+ }
+
+ /**
+ * Returns the selection of the list.
+ *
+ * @return returns an array of indices specifying the current selection.
+ */
+ public int[] getSelectionIndices() {
+ return fList.getSelectionIndices();
+ }
+
+ /**
+ * Returns the selection of the list. This is a convenience function for
+ * <code>getSelectionIndices()</code>.
+ *
+ * @return returns the index of the selection, -1 for no selection.
+ */
+ public int getSelectionIndex() {
+ return fList.getSelectionIndex();
+ }
+
+ /**
+ * Sets the selection of the list.
+ *
+ * @param elements
+ * the array of elements to be selected.
+ */
+ public void setSelection(Object[] elements) {
+ if ((elements == null) || (fElements == null))
+ return;
+
+ // fill indices
+ int[] indices = new int[elements.length];
+ for (int i = 0; i != elements.length; i++) {
+ int j;
+ for (j = 0; j != fFoldedCount; j++) {
+ int max = (j == fFoldedCount - 1) ? fFilteredCount
+ : fFoldedIndices[j + 1];
+
+ int l;
+ for (l = fFoldedIndices[j]; l != max; l++) {
+ // found matching element?
+ if (fElements[fFilteredIndices[l]].equals(elements[i])) {
+ indices[i] = j;
+ break;
+ }
+ }
+
+ if (l != max)
+ break;
+ }
+
+ // not found
+ if (j == fFoldedCount)
+ indices[i] = 0;
+ }
+
+ fList.setSelection(indices);
+ }
+
+ /**
+ * Returns an array of the selected elements. The type of the elements
+ * returned in the list are the same as the ones passed with
+ * <code>setElements</code>. The array does not contain the rendered
+ * strings.
+ *
+ * @return returns the array of selected elements.
+ */
+ public Object[] getSelection() {
+ if (fList.isDisposed() || (fList.getSelectionCount() == 0))
+ return new Object[0];
+
+ int[] indices = fList.getSelectionIndices();
+ Object[] elements = new Object[indices.length];
+
+ for (int i = 0; i != indices.length; i++)
+ elements[i] = fElements[fFilteredIndices[fFoldedIndices[indices[i]]]];
+
+ return elements;
+ }
+
+ /**
+ * Sets the filter pattern. Current only prefix filter patterns are
+ * supported.
+ *
+ * @param filter
+ * the filter pattern.
+ */
+ public void setFilter(String filter) {
+ fFilter = (filter == null) ? "" : filter; //$NON-NLS-1$
+
+ fFilteredCount = filter();
+ fFoldedCount = fold();
+ updateList();
+ }
+
+ /**
+ * Returns the filter pattern.
+ *
+ * @return returns the filter pattern.
+ */
+ public String getFilter() {
+ return fFilter;
+ }
+
+ /**
+ * Returns all elements which are folded together to one entry in the list.
+ *
+ * @param index
+ * the index selecting the entry in the list.
+ * @return returns an array of elements folded together, <code>null</code>
+ * if index is out of range.
+ */
+ public Object[] getFoldedElements(int index) {
+ if ((index < 0) || (index >= fFoldedCount))
+ return null;
+
+ int start = fFoldedIndices[index];
+ int count = (index == fFoldedCount - 1) ? fFilteredCount - start
+ : fFoldedIndices[index + 1] - start;
+
+ Object[] elements = new Object[count];
+ for (int i = 0; i != count; i++)
+ elements[i] = fElements[fFilteredIndices[start + i]];
+
+ return elements;
+ }
+
+ /*
+ * Folds duplicate entries. Two elements are considered as a pair of
+ * duplicates if they coiincide in the rendered string and image. @return
+ * returns the number of elements after folding.
+ */
+ private int fold() {
+ if (fAllowDuplicates) {
+ for (int i = 0; i != fFilteredCount; i++)
+ fFoldedIndices[i] = i; // identity mapping
+
+ return fFilteredCount;
+
+ } else {
+ int k = 0;
+ Label last = null;
+ for (int i = 0; i != fFilteredCount; i++) {
+ int j = fFilteredIndices[i];
+
+ Label current = fLabels[j];
+ if (!current.equals(last)) {
+ fFoldedIndices[k] = i;
+ k++;
+ last = current;
+ }
+ }
+ return k;
+ }
+ }
+
+ /*
+ * Filters the list with the filter pattern. @return returns the number of
+ * elements after filtering.
+ */
+ private int filter() {
+ if (((fFilter == null) || (fFilter.length() == 0))
+ && !fMatchEmtpyString)
+ return 0;
+
+ fFilterMatcher.setFilter(fFilter.trim(), fIgnoreCase, false);
+
+ int k = 0;
+ for (int i = 0; i != fElements.length; i++) {
+ if (fFilterMatcher.match(fElements[i]))
+ fFilteredIndices[k++] = i;
+ }
+
+ return k;
+ }
+
+ /*
+ * Updates the list widget.
+ */
+ private void updateList() {
+ if (fList.isDisposed())
+ return;
+
+ fList.setRedraw(false);
+
+ // resize table
+ int itemCount = fList.getItemCount();
+ if (fFoldedCount < itemCount)
+ fList.remove(0, itemCount - fFoldedCount - 1);
+ else if (fFoldedCount > itemCount)
+ for (int i = 0; i != fFoldedCount - itemCount; i++)
+ new TableItem(fList, SWT.NONE);
+
+ // fill table
+ TableItem[] items = fList.getItems();
+ for (int i = 0; i != fFoldedCount; i++) {
+ TableItem item = items[i];
+ Label label = fLabels[fFilteredIndices[fFoldedIndices[i]]];
+
+ item.setText(label.string);
+ item.setImage(label.image);
+ }
+
+ // select first item if any
+ if (fList.getItemCount() > 0)
+ fList.setSelection(0);
+
+ fList.setRedraw(true);
+ fList.notifyListeners(SWT.Selection, new Event());
+ }
+
+}
\ No newline at end of file