Improved completion processor
[phpeclipse.git] / net.sourceforge.phpeclipse / 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.
24  * The elements are sorted alphabetically.
25  * Optionally, the elements can be filtered and duplicate entries can
26  * be hidden (folding).
27  */
28 public class FilteredList extends Composite {
29
30         public interface FilterMatcher {
31                 /**
32                  * Sets the filter.
33                  * 
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.
37                  */
38                 void setFilter(String pattern, boolean ignoreCase, boolean ignoreWildCards);
39
40                 /**
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.
43                  */
44                 boolean match(Object element);  
45         }
46         
47         private class DefaultFilterMatcher implements FilterMatcher {
48                 private StringMatcher fMatcher;
49                 
50                 public void setFilter(String pattern, boolean ignoreCase, boolean ignoreWildCards) {
51                         fMatcher= new StringMatcher(pattern + '*', ignoreCase, ignoreWildCards);
52                 }
53                 
54                 public boolean match(Object element) {
55                         return fMatcher.match(fRenderer.getText(element));
56                 }       
57         }
58
59         private Table fList;
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;
66         
67         private Object[] fElements= new Object[0];
68         private Label[] fLabels;
69         private Vector fImages= new Vector();
70
71         private int[] fFoldedIndices;
72         private int fFoldedCount;
73         
74         private int[] fFilteredIndices;
75         private int fFilteredCount;
76         
77         private FilterMatcher fFilterMatcher= new DefaultFilterMatcher();
78         private Comparator fComparator;
79
80         private static class Label {
81                 public final String string;
82                 public final Image image;
83
84                 public Label(String string, Image image) {
85                         this.string= string;
86                         this.image= image;
87                 }
88                 
89                 public boolean equals(Label label) {
90                         if (label == null)
91                                 return false;
92                                 
93                         return                  
94                                 string.equals(label.string) &&
95                                 image.equals(label.image);
96                 }
97         }
98
99         private final class LabelComparator implements Comparator {
100                 private boolean fIgnoreCase;
101         
102                 LabelComparator(boolean ignoreCase) {
103                         fIgnoreCase= ignoreCase;
104                 }
105         
106                 public int compare(Object left, Object right) {
107                         Label leftLabel= (Label) left;
108                         Label rightLabel= (Label) right;                        
109
110                         int value;
111                         
112                         if (fComparator == null) {
113                                 value= fIgnoreCase
114                                         ? leftLabel.string.compareToIgnoreCase(rightLabel.string)
115                                         : leftLabel.string.compareTo(rightLabel.string);
116                         } else {
117                             value= fComparator.compare(leftLabel.string, rightLabel.string);
118                         }
119
120                         if (value != 0)
121                                 return value;
122
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) {
127                                 return +1;                              
128                         } else {
129                                 return
130                                         fImages.indexOf(leftLabel.image) -
131                                         fImages.indexOf(rightLabel.image);
132                         }
133                 }
134                 
135         }       
136         
137         /**
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.
145          */
146         public FilteredList(Composite parent, int style, ILabelProvider renderer,
147                 boolean ignoreCase, boolean allowDuplicates, boolean matchEmptyString)
148         {
149                 super(parent, SWT.NONE);
150
151                 GridLayout layout= new GridLayout();
152                 layout.marginHeight= 0;
153                 layout.marginWidth= 0;
154                 setLayout(layout);
155                 
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) {
160                                 fRenderer.dispose();
161                         }
162                 });
163                 
164                 fRenderer= renderer;
165                 fIgnoreCase= ignoreCase;                
166                 fSorter= new TwoArrayQuickSorter(new LabelComparator(ignoreCase));
167                 fAllowDuplicates= allowDuplicates;
168                 fMatchEmtpyString= matchEmptyString;
169         }
170
171         /**
172          * Sets the list of elements.
173          * @param elements the elements to be shown in the list.
174          */
175         public void setElements(Object[] elements) {
176                 if (elements == null) {
177                         fElements= new Object[0];
178                 } else {
179                         // copy list for sorting
180                         fElements= new Object[elements.length];
181                         System.arraycopy(elements, 0, fElements, 0, elements.length);
182                 }
183                         
184                 int length= fElements.length;
185
186                 // fill labels                  
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]);
192                         
193                         fLabels[i]= new Label(text, image);                             
194                         imageSet.add(image);
195                 }
196                 fImages.clear();
197                 fImages.addAll(imageSet);
198
199                 fSorter.sort(fLabels, fElements);
200
201                 fFilteredIndices= new int[length];
202                 fFilteredCount= filter();
203                 
204                 fFoldedIndices= new int[length];
205                 fFoldedCount= fold();
206
207                 updateList();
208         }
209
210         /**
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.
213          */
214         public boolean isEmpty() {
215                 return (fElements == null) || (fElements.length == 0);
216         }
217
218         /**
219          * Sets the filter matcher.
220          */
221         public void setFilterMatcher(FilterMatcher filterMatcher) {
222                 Assert.isNotNull(filterMatcher);
223                 fFilterMatcher= filterMatcher;
224         }
225         
226         /**
227          * Sets a custom comparator for sorting the list.
228          */
229         public void setComparator(Comparator comparator) {
230             Assert.isNotNull(comparator);
231             fComparator= comparator;
232         }
233
234     /**
235      * Adds a selection listener to the list.
236      * @param listener the selection listener to be added.
237      */
238         public void addSelectionListener(SelectionListener listener) {
239                 fList.addSelectionListener(listener);
240         }
241
242     /**
243      * Removes a selection listener from the list.
244      * @param listener the selection listener to be removed.
245      */
246         public void removeSelectionListener(SelectionListener listener) {
247                 fList.removeSelectionListener(listener);
248         }       
249
250     /**
251      * Sets the selection of the list.
252      * @param selection an array of indices specifying the selection.
253      */
254         public void setSelection(int[] selection) {
255                 fList.setSelection(selection);
256         }
257         
258         /**
259          * Returns the selection of the list.
260          * @return returns an array of indices specifying the current selection.
261          */
262         public int[] getSelectionIndices() {
263                 return fList.getSelectionIndices();
264         }
265         
266         /**
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.
270          */
271         public int getSelectionIndex() {
272                 return fList.getSelectionIndex();               
273         }
274         
275         /**
276          * Sets the selection of the list.
277          * @param elements the array of elements to be selected.
278          */
279         public void setSelection(Object[] elements) {
280                 if ((elements == null) || (fElements == null))
281                         return;                 
282                 
283                 // fill indices
284                 int[] indices= new int[elements.length];
285                 for (int i= 0; i != elements.length; i++) {
286                         int j;                  
287                         for (j= 0; j != fFoldedCount; j++) {
288                                 int max= (j == fFoldedCount - 1)
289                                         ? fFilteredCount
290                                         : fFoldedIndices[j + 1];
291
292                                 int l;                                  
293                                 for (l= fFoldedIndices[j]; l != max; l++) {
294                                         // found matching element?
295                                         if (fElements[fFilteredIndices[l]].equals(elements[i])) {
296                                                 indices[i]= j;
297                                                 break;  
298                                         }
299                                 }
300                                 
301                                 if (l != max)
302                                         break;
303                         }
304                         
305                         // not found
306                         if (j == fFoldedCount)
307                                 indices[i] = 0;
308                 }
309                 
310                 fList.setSelection(indices);
311         }
312         
313         /**
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.
318          */
319         public Object[] getSelection() {
320                 if (fList.isDisposed() || (fList.getSelectionCount() == 0))
321                         return new Object[0];
322
323                 int[] indices= fList.getSelectionIndices();
324                 Object[] elements= new Object[indices.length];
325                 
326                 for (int i= 0; i != indices.length; i++)
327                         elements[i]= fElements[fFilteredIndices[fFoldedIndices[indices[i]]]];
328                 
329                 return elements;                
330         }
331
332         /**
333          * Sets the filter pattern. Current only prefix filter patterns are supported.
334          * @param filter the filter pattern.
335          */
336         public void setFilter(String filter) {
337                 fFilter= (filter == null) ? "" : filter; //$NON-NLS-1$
338
339                 fFilteredCount= filter();
340                 fFoldedCount= fold();
341                 updateList();
342         }
343         
344         /**
345          * Returns the filter pattern.
346          * @return returns the filter pattern.
347          */
348         public String getFilter() {
349                 return fFilter;
350         }
351
352         /**
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.
356          */
357         public Object[] getFoldedElements(int index) {
358                 if ((index < 0) || (index >= fFoldedCount))
359                         return null;
360                 
361                 int start= fFoldedIndices[index];                       
362                 int count= (index == fFoldedCount - 1)
363                         ? fFilteredCount - start
364                         : fFoldedIndices[index + 1] - start;
365                         
366                 Object[] elements= new Object[count];
367                 for (int i= 0; i != count; i++)
368                         elements[i]= fElements[fFilteredIndices[start + i]];
369                                 
370                 return elements;
371         }
372
373     /*
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.
377      */
378         private int fold() {
379                 if (fAllowDuplicates) {
380                         for (int i= 0; i != fFilteredCount; i++)                
381                                 fFoldedIndices[i]= i; // identity mapping
382
383                         return fFilteredCount;                  
384                 
385                 } else {
386                         int k= 0;
387                         Label last= null;
388                         for (int i= 0; i != fFilteredCount; i++) {
389                                 int j= fFilteredIndices[i];
390                                 
391                                 Label current= fLabels[j];
392                                 if (! current.equals(last)) {
393                                         fFoldedIndices[k]= i;
394                                         k++;
395                                         last= current;
396                                 }
397                         }
398                         return k;
399                 }
400         }
401
402         /*
403          * Filters the list with the filter pattern.
404      * @return returns the number of elements after filtering.
405          */
406         private int filter() {
407                 if (((fFilter == null) || (fFilter.length() == 0)) && !fMatchEmtpyString)
408                         return 0;
409                 
410                 fFilterMatcher.setFilter(fFilter.trim(), fIgnoreCase, false);
411
412                 int k= 0;
413                 for (int i= 0; i != fElements.length; i++) {
414                         if (fFilterMatcher.match(fElements[i]))
415                                 fFilteredIndices[k++]= i;
416                 }                       
417                                                 
418                 return k;
419         }       
420
421         /*
422          * Updates the list widget.
423          */      
424         private void updateList() {
425                 if (fList.isDisposed())
426                         return;
427                         
428                 fList.setRedraw(false);
429                 
430                 // resize table
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);
437
438                 // fill table
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]]];
443                         
444                         item.setText(label.string);
445                         item.setImage(label.image);
446                 }
447
448                 // select first item if any
449                 if (fList.getItemCount() > 0)
450                         fList.setSelection(0);
451                         
452                 fList.setRedraw(true);          
453                 fList.notifyListeners(SWT.Selection, new Event());
454         }
455         
456 }