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