package org.rubypeople.rdt.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; } 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()); } }