| /* |
| * (c) Copyright IBM Corp. 2000, 2002. All Rights Reserved. |
| * Contributors: Sebastian Davids <sdavids@gmx.de> - Fix for bug 19346 - Dialog |
| * font should be activated and used by other components. |
| */ |
| package org.eclipse.ui.dialogs; |
| |
| import java.util.Comparator; |
| import java.util.HashSet; |
| import java.util.Set; |
| import java.util.Vector; |
| |
| 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.Display; |
| import org.eclipse.swt.widgets.Event; |
| import org.eclipse.swt.widgets.Table; |
| import org.eclipse.swt.widgets.TableItem; |
| |
| import org.eclipse.jface.util.Assert; |
| import org.eclipse.jface.viewers.ILabelProvider; |
| |
| import org.eclipse.ui.internal.misc.StringMatcher; |
| |
| /** |
| * 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). |
| * |
| * @since 2.0 |
| */ |
| 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(fLabelProvider.getText(element)); |
| } |
| } |
| |
| private Table fList; |
| private ILabelProvider fLabelProvider; |
| private boolean fMatchEmptyString= 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 UpdateThread fUpdateThread; |
| |
| 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 filtered list. |
| * |
| * @param parent the parent composite |
| * @param style the widget style |
| * @param labelProvider 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 labelProvider, |
| 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.setFont(parent.getFont()); |
| fList.addDisposeListener(new DisposeListener() { |
| public void widgetDisposed(DisposeEvent e) { |
| fLabelProvider.dispose(); |
| if (fUpdateThread != null) |
| fUpdateThread.requestStop(); |
| } |
| }); |
| |
| fLabelProvider= labelProvider; |
| fIgnoreCase= ignoreCase; |
| fSorter= new TwoArrayQuickSorter(new LabelComparator(ignoreCase)); |
| fAllowDuplicates= allowDuplicates; |
| fMatchEmptyString= 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= fLabelProvider.getText(fElements[i]); |
| Image image= fLabelProvider.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]; |
| fFoldedIndices= new int[length]; |
| |
| 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. |
| * Empty or null array removes selection. |
| * @param selection an array of indices specifying the selection. |
| */ |
| public void setSelection(int[] selection) { |
| if (selection == null || selection.length == 0) |
| fList.deselectAll(); |
| else |
| 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. |
| * Empty or null array removes selection. |
| * @param elements the array of elements to be selected. |
| */ |
| public void setSelection(Object[] elements) { |
| if (elements == null || elements.length == 0) { |
| fList.deselectAll(); |
| return; |
| } |
| |
| if (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$ |
| |
| updateList(); |
| } |
| |
| private void updateList() { |
| fFilteredCount= filter(); |
| fFoldedCount= fold(); |
| |
| if (fUpdateThread != null) |
| fUpdateThread.requestStop(); |
| fUpdateThread= new UpdateThread(new TableUpdater(fList, fFoldedCount)); |
| fUpdateThread.start(); |
| } |
| |
| /** |
| * 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)) && !fMatchEmptyString) |
| 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; |
| } |
| |
| private interface IncrementalRunnable extends Runnable { |
| public int getCount(); |
| public void cancel(); |
| } |
| |
| private class TableUpdater implements IncrementalRunnable { |
| private final Display fDisplay; |
| private final Table fTable; |
| private final int fCount; |
| private int fIndex; |
| |
| public TableUpdater(Table table, int count) { |
| fTable= table; |
| fDisplay= table.getDisplay(); |
| fCount= count; |
| } |
| |
| /* |
| * @see IncrementalRunnable#getCount() |
| */ |
| public int getCount() { |
| return fCount + 1; |
| } |
| |
| /* |
| * @see IncrementalRunnable#cancel() |
| */ |
| public void cancel() { |
| fIndex= 0; |
| } |
| |
| /* |
| * @see Runnable#run() |
| */ |
| public void run() { |
| final int index= fIndex++; |
| |
| fDisplay.syncExec(new Runnable() { |
| public void run() { |
| if (fTable.isDisposed()) |
| return; |
| |
| final int itemCount= fTable.getItemCount(); |
| |
| if (index < fCount) { |
| final TableItem item= (index < itemCount) |
| ? fTable.getItem(index) |
| : new TableItem(fTable, SWT.NONE); |
| |
| final Label label= fLabels[fFilteredIndices[fFoldedIndices[index]]]; |
| |
| item.setText(label.string); |
| item.setImage(label.image); |
| |
| // select first item |
| if (index == 0) { |
| fTable.setSelection(0); |
| fTable.notifyListeners(SWT.Selection, new Event()); |
| } |
| |
| // finish |
| } else { |
| if (fCount < itemCount) { |
| fTable.setRedraw(false); |
| fTable.remove(fCount, itemCount - 1); |
| fTable.setRedraw(true); |
| } |
| |
| // table empty -> no selection |
| if (fCount == 0) |
| fTable.notifyListeners(SWT.Selection, new Event()); |
| } |
| } |
| }); |
| } |
| } |
| |
| private static class UpdateThread extends Thread { |
| |
| /** The incremental runnable */ |
| private final IncrementalRunnable fRunnable; |
| /** A flag indicating a thread stop request */ |
| private boolean fStop; |
| |
| /** |
| * Creates an update thread. |
| */ |
| public UpdateThread(IncrementalRunnable runnable) { |
| fRunnable= runnable; |
| } |
| |
| /** |
| * Requests the thread to stop. |
| */ |
| public void requestStop() { |
| fStop= true; |
| } |
| |
| /** |
| * @see Runnable#run() |
| */ |
| public void run() { |
| final int count= fRunnable.getCount(); |
| for (int i= 0; i != count; i++) { |
| if (i % 50 == 0) |
| try { Thread.sleep(10); } catch (InterruptedException e) {} |
| |
| if (fStop) { |
| fRunnable.cancel(); |
| break; |
| } |
| |
| fRunnable.run(); |
| } |
| } |
| } |
| |
| /** |
| * Returns whether or not duplicates are allowed. |
| * |
| * @return <code>true</code> indicates duplicates are allowed |
| */ |
| public boolean getAllowDuplicates() { |
| return fAllowDuplicates; |
| } |
| |
| /** |
| * Sets whether or not duplicates are allowed. |
| * If this value is set the items should be set again for this value |
| * to take effect. |
| * |
| * @param allowDuplicates <code>true</code> indicates duplicates are allowed |
| */ |
| public void setAllowDuplicates(boolean allowDuplicates) { |
| this.fAllowDuplicates = allowDuplicates; |
| } |
| |
| /** |
| * Returns whether or not case should be ignored. |
| * |
| * @return <code>true</code> if case should be ignored |
| */ |
| public boolean getIgnoreCase() { |
| return fIgnoreCase; |
| } |
| |
| /** |
| * Sets whether or not case should be ignored |
| * If this value is set the items should be set again for this value |
| * to take effect. |
| * |
| * @param ignoreCase <code>true</code> if case should be ignored |
| */ |
| public void setIgnoreCase(boolean ignoreCase) { |
| this.fIgnoreCase = ignoreCase; |
| } |
| |
| /** |
| * Returns whether empty filter strings should filter everything or nothing. |
| * |
| * @return <code>true</code> for the empty string to |
| * match all items, <code>false</code> to match none |
| */ |
| public boolean getMatchEmptyString() { |
| return fMatchEmptyString; |
| } |
| |
| /** |
| * Sets whether empty filter strings should filter everything or nothing. |
| * If this value is set the items should be set again for this value |
| * to take effect. |
| * |
| * @param matchEmptyString <code>true</code> for the empty string to |
| * match all items, <code>false</code> to match none |
| */ |
| public void setMatchEmptyString(boolean matchEmptyString) { |
| this.fMatchEmptyString = matchEmptyString; |
| } |
| |
| /** |
| * Returns the label provider for the items. |
| * |
| * @return the label provider |
| */ |
| public ILabelProvider getLabelProvider() { |
| return fLabelProvider; |
| } |
| |
| /** |
| * Sets the label provider. |
| * If this value is set the items should be set again for this value |
| * to take effect. |
| * |
| * @param labelProvider the label provider |
| */ |
| public void setLabelProvider(ILabelProvider labelProvider) { |
| this.fLabelProvider = labelProvider; |
| } |
| |
| } |