Improved completion processor
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpdt / internal / ui / util / FilteredList.java
diff --git a/net.sourceforge.phpeclipse/src/net/sourceforge/phpdt/internal/ui/util/FilteredList.java b/net.sourceforge.phpeclipse/src/net/sourceforge/phpdt/internal/ui/util/FilteredList.java
new file mode 100644 (file)
index 0000000..892c2df
--- /dev/null
@@ -0,0 +1,456 @@
+package net.sourceforge.phpdt.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;                  
+               
+               } else {
+                       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());
+       }
+       
+}
\ No newline at end of file