1 package net.sourceforge.phpdt.internal.ui.util;
3 import java.util.Comparator;
4 import java.util.HashSet;
6 import java.util.Vector;
8 import org.eclipse.jface.util.Assert;
9 import org.eclipse.jface.viewers.ILabelProvider;
10 import org.eclipse.swt.SWT;
11 import org.eclipse.swt.events.DisposeEvent;
12 import org.eclipse.swt.events.DisposeListener;
13 import org.eclipse.swt.events.SelectionListener;
14 import org.eclipse.swt.graphics.Image;
15 import org.eclipse.swt.layout.GridData;
16 import org.eclipse.swt.layout.GridLayout;
17 import org.eclipse.swt.widgets.Composite;
18 import org.eclipse.swt.widgets.Event;
19 import org.eclipse.swt.widgets.Table;
20 import org.eclipse.swt.widgets.TableItem;
23 * A composite widget which holds a list of elements for user selection.
24 * The elements are sorted alphabetically.
25 * Optionally, the elements can be filtered and duplicate entries can
26 * be hidden (folding).
28 public class FilteredList extends Composite {
30 public interface FilterMatcher {
34 * @param pattern the filter pattern.
35 * @param ignoreCase a flag indicating whether pattern matching is case insensitive or not.
36 * @param ignoreWildCards a flag indicating whether wildcard characters are interpreted or not.
38 void setFilter(String pattern, boolean ignoreCase, boolean ignoreWildCards);
41 * Returns <code>true</code> if the object matches the pattern, <code>false</code> otherwise.
42 * <code>setFilter()</code> must have been called at least once prior to a call to this method.
44 boolean match(Object element);
47 private class DefaultFilterMatcher implements FilterMatcher {
48 private StringMatcher fMatcher;
50 public void setFilter(String pattern, boolean ignoreCase, boolean ignoreWildCards) {
51 fMatcher= new StringMatcher(pattern + '*', ignoreCase, ignoreWildCards);
54 public boolean match(Object element) {
55 return fMatcher.match(fRenderer.getText(element));
60 private ILabelProvider fRenderer;
61 private boolean fMatchEmtpyString= true;
62 private boolean fIgnoreCase;
63 private boolean fAllowDuplicates;
64 private String fFilter= ""; //$NON-NLS-1$
65 private TwoArrayQuickSorter fSorter;
67 private Object[] fElements= new Object[0];
68 private Label[] fLabels;
69 private Vector fImages= new Vector();
71 private int[] fFoldedIndices;
72 private int fFoldedCount;
74 private int[] fFilteredIndices;
75 private int fFilteredCount;
77 private FilterMatcher fFilterMatcher= new DefaultFilterMatcher();
78 private Comparator fComparator;
80 private static class Label {
81 public final String string;
82 public final Image image;
84 public Label(String string, Image image) {
89 public boolean equals(Label label) {
94 string.equals(label.string) &&
95 image.equals(label.image);
99 private final class LabelComparator implements Comparator {
100 private boolean fIgnoreCase;
102 LabelComparator(boolean ignoreCase) {
103 fIgnoreCase= ignoreCase;
106 public int compare(Object left, Object right) {
107 Label leftLabel= (Label) left;
108 Label rightLabel= (Label) right;
112 if (fComparator == null) {
114 ? leftLabel.string.compareToIgnoreCase(rightLabel.string)
115 : leftLabel.string.compareTo(rightLabel.string);
117 value= fComparator.compare(leftLabel.string, rightLabel.string);
123 // images are allowed to be null
124 if (leftLabel.image == null) {
125 return (rightLabel.image == null) ? 0 : -1;
126 } else if (rightLabel.image == null) {
130 fImages.indexOf(leftLabel.image) -
131 fImages.indexOf(rightLabel.image);
138 * Constructs a new instance of a filtered list.
139 * @param parent the parent composite.
140 * @param style the widget style.
141 * @param renderer the label renderer.
142 * @param ignoreCase specifies whether sorting and folding is case sensitive.
143 * @param allowDuplicates specifies whether folding of duplicates is desired.
144 * @param matchEmptyString specifies whether empty filter strings should filter everything or nothing.
146 public FilteredList(Composite parent, int style, ILabelProvider renderer,
147 boolean ignoreCase, boolean allowDuplicates, boolean matchEmptyString)
149 super(parent, SWT.NONE);
151 GridLayout layout= new GridLayout();
152 layout.marginHeight= 0;
153 layout.marginWidth= 0;
156 fList= new Table(this, style);
157 fList.setLayoutData(new GridData(GridData.FILL_BOTH));
158 fList.addDisposeListener(new DisposeListener() {
159 public void widgetDisposed(DisposeEvent e) {
165 fIgnoreCase= ignoreCase;
166 fSorter= new TwoArrayQuickSorter(new LabelComparator(ignoreCase));
167 fAllowDuplicates= allowDuplicates;
168 fMatchEmtpyString= matchEmptyString;
172 * Sets the list of elements.
173 * @param elements the elements to be shown in the list.
175 public void setElements(Object[] elements) {
176 if (elements == null) {
177 fElements= new Object[0];
179 // copy list for sorting
180 fElements= new Object[elements.length];
181 System.arraycopy(elements, 0, fElements, 0, elements.length);
184 int length= fElements.length;
187 fLabels= new Label[length];
188 Set imageSet= new HashSet();
189 for (int i= 0; i != length; i++) {
190 String text= fRenderer.getText(fElements[i]);
191 Image image= fRenderer.getImage(fElements[i]);
193 fLabels[i]= new Label(text, image);
197 fImages.addAll(imageSet);
199 fSorter.sort(fLabels, fElements);
201 fFilteredIndices= new int[length];
202 fFilteredCount= filter();
204 fFoldedIndices= new int[length];
205 fFoldedCount= fold();
211 * Tests if the list (before folding and filtering) is empty.
212 * @return returns <code>true</code> if the list is empty, <code>false</code> otherwise.
214 public boolean isEmpty() {
215 return (fElements == null) || (fElements.length == 0);
219 * Sets the filter matcher.
221 public void setFilterMatcher(FilterMatcher filterMatcher) {
222 Assert.isNotNull(filterMatcher);
223 fFilterMatcher= filterMatcher;
227 * Sets a custom comparator for sorting the list.
229 public void setComparator(Comparator comparator) {
230 Assert.isNotNull(comparator);
231 fComparator= comparator;
235 * Adds a selection listener to the list.
236 * @param listener the selection listener to be added.
238 public void addSelectionListener(SelectionListener listener) {
239 fList.addSelectionListener(listener);
243 * Removes a selection listener from the list.
244 * @param listener the selection listener to be removed.
246 public void removeSelectionListener(SelectionListener listener) {
247 fList.removeSelectionListener(listener);
251 * Sets the selection of the list.
252 * @param selection an array of indices specifying the selection.
254 public void setSelection(int[] selection) {
255 fList.setSelection(selection);
259 * Returns the selection of the list.
260 * @return returns an array of indices specifying the current selection.
262 public int[] getSelectionIndices() {
263 return fList.getSelectionIndices();
267 * Returns the selection of the list.
268 * This is a convenience function for <code>getSelectionIndices()</code>.
269 * @return returns the index of the selection, -1 for no selection.
271 public int getSelectionIndex() {
272 return fList.getSelectionIndex();
276 * Sets the selection of the list.
277 * @param elements the array of elements to be selected.
279 public void setSelection(Object[] elements) {
280 if ((elements == null) || (fElements == null))
284 int[] indices= new int[elements.length];
285 for (int i= 0; i != elements.length; i++) {
287 for (j= 0; j != fFoldedCount; j++) {
288 int max= (j == fFoldedCount - 1)
290 : fFoldedIndices[j + 1];
293 for (l= fFoldedIndices[j]; l != max; l++) {
294 // found matching element?
295 if (fElements[fFilteredIndices[l]].equals(elements[i])) {
306 if (j == fFoldedCount)
310 fList.setSelection(indices);
314 * Returns an array of the selected elements. The type of the elements
315 * returned in the list are the same as the ones passed with
316 * <code>setElements</code>. The array does not contain the rendered strings.
317 * @return returns the array of selected elements.
319 public Object[] getSelection() {
320 if (fList.isDisposed() || (fList.getSelectionCount() == 0))
321 return new Object[0];
323 int[] indices= fList.getSelectionIndices();
324 Object[] elements= new Object[indices.length];
326 for (int i= 0; i != indices.length; i++)
327 elements[i]= fElements[fFilteredIndices[fFoldedIndices[indices[i]]]];
333 * Sets the filter pattern. Current only prefix filter patterns are supported.
334 * @param filter the filter pattern.
336 public void setFilter(String filter) {
337 fFilter= (filter == null) ? "" : filter; //$NON-NLS-1$
339 fFilteredCount= filter();
340 fFoldedCount= fold();
345 * Returns the filter pattern.
346 * @return returns the filter pattern.
348 public String getFilter() {
353 * Returns all elements which are folded together to one entry in the list.
354 * @param index the index selecting the entry in the list.
355 * @return returns an array of elements folded together, <code>null</code> if index is out of range.
357 public Object[] getFoldedElements(int index) {
358 if ((index < 0) || (index >= fFoldedCount))
361 int start= fFoldedIndices[index];
362 int count= (index == fFoldedCount - 1)
363 ? fFilteredCount - start
364 : fFoldedIndices[index + 1] - start;
366 Object[] elements= new Object[count];
367 for (int i= 0; i != count; i++)
368 elements[i]= fElements[fFilteredIndices[start + i]];
374 * Folds duplicate entries. Two elements are considered as a pair of
375 * duplicates if they coiincide in the rendered string and image.
376 * @return returns the number of elements after folding.
379 if (fAllowDuplicates) {
380 for (int i= 0; i != fFilteredCount; i++)
381 fFoldedIndices[i]= i; // identity mapping
383 return fFilteredCount;
388 for (int i= 0; i != fFilteredCount; i++) {
389 int j= fFilteredIndices[i];
391 Label current= fLabels[j];
392 if (! current.equals(last)) {
393 fFoldedIndices[k]= i;
403 * Filters the list with the filter pattern.
404 * @return returns the number of elements after filtering.
406 private int filter() {
407 if (((fFilter == null) || (fFilter.length() == 0)) && !fMatchEmtpyString)
410 fFilterMatcher.setFilter(fFilter.trim(), fIgnoreCase, false);
413 for (int i= 0; i != fElements.length; i++) {
414 if (fFilterMatcher.match(fElements[i]))
415 fFilteredIndices[k++]= i;
422 * Updates the list widget.
424 private void updateList() {
425 if (fList.isDisposed())
428 fList.setRedraw(false);
431 int itemCount= fList.getItemCount();
432 if (fFoldedCount < itemCount)
433 fList.remove(0, itemCount - fFoldedCount - 1);
434 else if (fFoldedCount > itemCount)
435 for (int i= 0; i != fFoldedCount - itemCount; i++)
436 new TableItem(fList, SWT.NONE);
439 TableItem[] items= fList.getItems();
440 for (int i= 0; i != fFoldedCount; i++) {
441 TableItem item= items[i];
442 Label label= fLabels[fFilteredIndices[fFoldedIndices[i]]];
444 item.setText(label.string);
445 item.setImage(label.image);
448 // select first item if any
449 if (fList.getItemCount() > 0)
450 fList.setSelection(0);
452 fList.setRedraw(true);
453 fList.notifyListeners(SWT.Selection, new Event());