65b17f6e7dcb5c222a062ccea28f9420a89f905f
[phpeclipse.git] / net.sourceforge.phpeclipse.ui / src / net / sourceforge / phpdt / internal / ui / util / FilteredList.java
1 package net.sourceforge.phpdt.internal.ui.util;
2
3 import java.util.Comparator;
4 import java.util.HashSet;
5 import java.util.Set;
6 import java.util.Vector;
7
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;
21
22 /**
23  * A composite widget which holds a list of elements for user selection. The
24  * elements are sorted alphabetically. Optionally, the elements can be filtered
25  * and duplicate entries can be hidden (folding).
26  */
27 public class FilteredList extends Composite {
28
29         public interface FilterMatcher {
30                 /**
31                  * Sets the filter.
32                  * 
33                  * @param pattern
34                  *            the filter pattern.
35                  * @param ignoreCase
36                  *            a flag indicating whether pattern matching is case
37                  *            insensitive or not.
38                  * @param ignoreWildCards
39                  *            a flag indicating whether wildcard characters are
40                  *            interpreted or not.
41                  */
42                 void setFilter(String pattern, boolean ignoreCase,
43                                 boolean ignoreWildCards);
44
45                 /**
46                  * Returns <code>true</code> if the object matches the pattern,
47                  * <code>false</code> otherwise. <code>setFilter()</code> must have
48                  * been called at least once prior to a call to this method.
49                  */
50                 boolean match(Object element);
51         }
52
53         private class DefaultFilterMatcher implements FilterMatcher {
54                 private StringMatcher fMatcher;
55
56                 public void setFilter(String pattern, boolean ignoreCase,
57                                 boolean ignoreWildCards) {
58                         fMatcher = new StringMatcher(pattern + '*', ignoreCase,
59                                         ignoreWildCards);
60                 }
61
62                 public boolean match(Object element) {
63                         return fMatcher.match(fRenderer.getText(element));
64                 }
65         }
66
67         private Table fList;
68
69         private ILabelProvider fRenderer;
70
71         private boolean fMatchEmtpyString = true;
72
73         private boolean fIgnoreCase;
74
75         private boolean fAllowDuplicates;
76
77         private String fFilter = ""; //$NON-NLS-1$
78
79         private TwoArrayQuickSorter fSorter;
80
81         private Object[] fElements = new Object[0];
82
83         private Label[] fLabels;
84
85         private Vector fImages = new Vector();
86
87         private int[] fFoldedIndices;
88
89         private int fFoldedCount;
90
91         private int[] fFilteredIndices;
92
93         private int fFilteredCount;
94
95         private FilterMatcher fFilterMatcher = new DefaultFilterMatcher();
96
97         private Comparator fComparator;
98
99         private static class Label {
100                 public final String string;
101
102                 public final Image image;
103
104                 public Label(String string, Image image) {
105                         this.string = string;
106                         this.image = image;
107                 }
108
109                 public boolean equals(Label label) {
110                         if (label == null)
111                                 return false;
112
113                         return string.equals(label.string) && image.equals(label.image);
114                 }
115         }
116
117         private final class LabelComparator implements Comparator {
118                 private boolean fIgnoreCase;
119
120                 LabelComparator(boolean ignoreCase) {
121                         fIgnoreCase = ignoreCase;
122                 }
123
124                 public int compare(Object left, Object right) {
125                         Label leftLabel = (Label) left;
126                         Label rightLabel = (Label) right;
127
128                         int value;
129
130                         if (fComparator == null) {
131                                 value = fIgnoreCase ? leftLabel.string
132                                                 .compareToIgnoreCase(rightLabel.string)
133                                                 : leftLabel.string.compareTo(rightLabel.string);
134                         } else {
135                                 value = fComparator
136                                                 .compare(leftLabel.string, rightLabel.string);
137                         }
138
139                         if (value != 0)
140                                 return value;
141
142                         // images are allowed to be null
143                         if (leftLabel.image == null) {
144                                 return (rightLabel.image == null) ? 0 : -1;
145                         } else if (rightLabel.image == null) {
146                                 return +1;
147                         } else {
148                                 return fImages.indexOf(leftLabel.image)
149                                                 - fImages.indexOf(rightLabel.image);
150                         }
151                 }
152
153         }
154
155         /**
156          * Constructs a new instance of a filtered list.
157          * 
158          * @param parent
159          *            the parent composite.
160          * @param style
161          *            the widget style.
162          * @param renderer
163          *            the label renderer.
164          * @param ignoreCase
165          *            specifies whether sorting and folding is case sensitive.
166          * @param allowDuplicates
167          *            specifies whether folding of duplicates is desired.
168          * @param matchEmptyString
169          *            specifies whether empty filter strings should filter
170          *            everything or nothing.
171          */
172         public FilteredList(Composite parent, int style, ILabelProvider renderer,
173                         boolean ignoreCase, boolean allowDuplicates,
174                         boolean matchEmptyString) {
175                 super(parent, SWT.NONE);
176
177                 GridLayout layout = new GridLayout();
178                 layout.marginHeight = 0;
179                 layout.marginWidth = 0;
180                 setLayout(layout);
181
182                 fList = new Table(this, style);
183                 fList.setLayoutData(new GridData(GridData.FILL_BOTH));
184                 fList.addDisposeListener(new DisposeListener() {
185                         public void widgetDisposed(DisposeEvent e) {
186                                 fRenderer.dispose();
187                         }
188                 });
189
190                 fRenderer = renderer;
191                 fIgnoreCase = ignoreCase;
192                 fSorter = new TwoArrayQuickSorter(new LabelComparator(ignoreCase));
193                 fAllowDuplicates = allowDuplicates;
194                 fMatchEmtpyString = matchEmptyString;
195         }
196
197         /**
198          * Sets the list of elements.
199          * 
200          * @param elements
201          *            the elements to be shown in the list.
202          */
203         public void setElements(Object[] elements) {
204                 if (elements == null) {
205                         fElements = new Object[0];
206                 } else {
207                         // copy list for sorting
208                         fElements = new Object[elements.length];
209                         System.arraycopy(elements, 0, fElements, 0, elements.length);
210                 }
211
212                 int length = fElements.length;
213
214                 // fill labels
215                 fLabels = new Label[length];
216                 Set imageSet = new HashSet();
217                 for (int i = 0; i != length; i++) {
218                         String text = fRenderer.getText(fElements[i]);
219                         Image image = fRenderer.getImage(fElements[i]);
220
221                         fLabels[i] = new Label(text, image);
222                         imageSet.add(image);
223                 }
224                 fImages.clear();
225                 fImages.addAll(imageSet);
226
227                 fSorter.sort(fLabels, fElements);
228
229                 fFilteredIndices = new int[length];
230                 fFilteredCount = filter();
231
232                 fFoldedIndices = new int[length];
233                 fFoldedCount = fold();
234
235                 updateList();
236         }
237
238         /**
239          * Tests if the list (before folding and filtering) is empty.
240          * 
241          * @return returns <code>true</code> if the list is empty,
242          *         <code>false</code> otherwise.
243          */
244         public boolean isEmpty() {
245                 return (fElements == null) || (fElements.length == 0);
246         }
247
248         /**
249          * Sets the filter matcher.
250          */
251         public void setFilterMatcher(FilterMatcher filterMatcher) {
252                 Assert.isNotNull(filterMatcher);
253                 fFilterMatcher = filterMatcher;
254         }
255
256         /**
257          * Sets a custom comparator for sorting the list.
258          */
259         public void setComparator(Comparator comparator) {
260                 Assert.isNotNull(comparator);
261                 fComparator = comparator;
262         }
263
264         /**
265          * Adds a selection listener to the list.
266          * 
267          * @param listener
268          *            the selection listener to be added.
269          */
270         public void addSelectionListener(SelectionListener listener) {
271                 fList.addSelectionListener(listener);
272         }
273
274         /**
275          * Removes a selection listener from the list.
276          * 
277          * @param listener
278          *            the selection listener to be removed.
279          */
280         public void removeSelectionListener(SelectionListener listener) {
281                 fList.removeSelectionListener(listener);
282         }
283
284         /**
285          * Sets the selection of the list.
286          * 
287          * @param selection
288          *            an array of indices specifying the selection.
289          */
290         public void setSelection(int[] selection) {
291                 fList.setSelection(selection);
292         }
293
294         /**
295          * Returns the selection of the list.
296          * 
297          * @return returns an array of indices specifying the current selection.
298          */
299         public int[] getSelectionIndices() {
300                 return fList.getSelectionIndices();
301         }
302
303         /**
304          * Returns the selection of the list. This is a convenience function for
305          * <code>getSelectionIndices()</code>.
306          * 
307          * @return returns the index of the selection, -1 for no selection.
308          */
309         public int getSelectionIndex() {
310                 return fList.getSelectionIndex();
311         }
312
313         /**
314          * Sets the selection of the list.
315          * 
316          * @param elements
317          *            the array of elements to be selected.
318          */
319         public void setSelection(Object[] elements) {
320                 if ((elements == null) || (fElements == null))
321                         return;
322
323                 // fill indices
324                 int[] indices = new int[elements.length];
325                 for (int i = 0; i != elements.length; i++) {
326                         int j;
327                         for (j = 0; j != fFoldedCount; j++) {
328                                 int max = (j == fFoldedCount - 1) ? fFilteredCount
329                                                 : fFoldedIndices[j + 1];
330
331                                 int l;
332                                 for (l = fFoldedIndices[j]; l != max; l++) {
333                                         // found matching element?
334                                         if (fElements[fFilteredIndices[l]].equals(elements[i])) {
335                                                 indices[i] = j;
336                                                 break;
337                                         }
338                                 }
339
340                                 if (l != max)
341                                         break;
342                         }
343
344                         // not found
345                         if (j == fFoldedCount)
346                                 indices[i] = 0;
347                 }
348
349                 fList.setSelection(indices);
350         }
351
352         /**
353          * Returns an array of the selected elements. The type of the elements
354          * returned in the list are the same as the ones passed with
355          * <code>setElements</code>. The array does not contain the rendered
356          * strings.
357          * 
358          * @return returns the array of selected elements.
359          */
360         public Object[] getSelection() {
361                 if (fList.isDisposed() || (fList.getSelectionCount() == 0))
362                         return new Object[0];
363
364                 int[] indices = fList.getSelectionIndices();
365                 Object[] elements = new Object[indices.length];
366
367                 for (int i = 0; i != indices.length; i++)
368                         elements[i] = fElements[fFilteredIndices[fFoldedIndices[indices[i]]]];
369
370                 return elements;
371         }
372
373         /**
374          * Sets the filter pattern. Current only prefix filter patterns are
375          * supported.
376          * 
377          * @param filter
378          *            the filter pattern.
379          */
380         public void setFilter(String filter) {
381                 fFilter = (filter == null) ? "" : filter; //$NON-NLS-1$
382
383                 fFilteredCount = filter();
384                 fFoldedCount = fold();
385                 updateList();
386         }
387
388         /**
389          * Returns the filter pattern.
390          * 
391          * @return returns the filter pattern.
392          */
393         public String getFilter() {
394                 return fFilter;
395         }
396
397         /**
398          * Returns all elements which are folded together to one entry in the list.
399          * 
400          * @param index
401          *            the index selecting the entry in the list.
402          * @return returns an array of elements folded together, <code>null</code>
403          *         if index is out of range.
404          */
405         public Object[] getFoldedElements(int index) {
406                 if ((index < 0) || (index >= fFoldedCount))
407                         return null;
408
409                 int start = fFoldedIndices[index];
410                 int count = (index == fFoldedCount - 1) ? fFilteredCount - start
411                                 : fFoldedIndices[index + 1] - start;
412
413                 Object[] elements = new Object[count];
414                 for (int i = 0; i != count; i++)
415                         elements[i] = fElements[fFilteredIndices[start + i]];
416
417                 return elements;
418         }
419
420         /*
421          * Folds duplicate entries. Two elements are considered as a pair of
422          * duplicates if they coiincide in the rendered string and image. @return
423          * returns the number of elements after folding.
424          */
425         private int fold() {
426                 if (fAllowDuplicates) {
427                         for (int i = 0; i != fFilteredCount; i++)
428                                 fFoldedIndices[i] = i; // identity mapping
429
430                         return fFilteredCount;
431
432                 } else {
433                         int k = 0;
434                         Label last = null;
435                         for (int i = 0; i != fFilteredCount; i++) {
436                                 int j = fFilteredIndices[i];
437
438                                 Label current = fLabels[j];
439                                 if (!current.equals(last)) {
440                                         fFoldedIndices[k] = i;
441                                         k++;
442                                         last = current;
443                                 }
444                         }
445                         return k;
446                 }
447         }
448
449         /*
450          * Filters the list with the filter pattern. @return returns the number of
451          * elements after filtering.
452          */
453         private int filter() {
454                 if (((fFilter == null) || (fFilter.length() == 0))
455                                 && !fMatchEmtpyString)
456                         return 0;
457
458                 fFilterMatcher.setFilter(fFilter.trim(), fIgnoreCase, false);
459
460                 int k = 0;
461                 for (int i = 0; i != fElements.length; i++) {
462                         if (fFilterMatcher.match(fElements[i]))
463                                 fFilteredIndices[k++] = i;
464                 }
465
466                 return k;
467         }
468
469         /*
470          * Updates the list widget.
471          */
472         private void updateList() {
473                 if (fList.isDisposed())
474                         return;
475
476                 fList.setRedraw(false);
477
478                 // resize table
479                 int itemCount = fList.getItemCount();
480                 if (fFoldedCount < itemCount)
481                         fList.remove(0, itemCount - fFoldedCount - 1);
482                 else if (fFoldedCount > itemCount)
483                         for (int i = 0; i != fFoldedCount - itemCount; i++)
484                                 new TableItem(fList, SWT.NONE);
485
486                 // fill table
487                 TableItem[] items = fList.getItems();
488                 for (int i = 0; i != fFoldedCount; i++) {
489                         TableItem item = items[i];
490                         Label label = fLabels[fFilteredIndices[fFoldedIndices[i]]];
491
492                         item.setText(label.string);
493                         item.setImage(label.image);
494                 }
495
496                 // select first item if any
497                 if (fList.getItemCount() > 0)
498                         fList.setSelection(0);
499
500                 fList.setRedraw(true);
501                 fList.notifyListeners(SWT.Selection, new Event());
502         }
503
504 }