blob: 29241b914d10f0e80b528698228842191d16f362 [file] [log] [blame]
/*
* (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;
}
}