blob: 39c3b256a8f22175f57bdbe9902dee3160938f39 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2006 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* 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.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.accessibility.Accessible;
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;
import org.eclipse.ui.internal.WorkbenchMessages;
import org.eclipse.ui.internal.misc.StringMatcher;
import org.eclipse.ui.internal.util.Util;
import org.eclipse.ui.progress.WorkbenchJob;
/**
* 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 {
/**
* The FilterMatcher is the interface used to check filtering criterea.
*/
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);
/**
* @param element
* The element to test against.
* @return <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;
ILabelProvider fLabelProvider;
private boolean fMatchEmptyString = true;
private boolean fIgnoreCase;
private boolean fAllowDuplicates;
private String fFilter = ""; //$NON-NLS-1$
private TwoArrayQuickSorter fSorter;
Object[] fElements = new Object[0];
Label[] fLabels;
Vector fImages = new Vector();
int[] fFoldedIndices;
int fFoldedCount;
int[] fFilteredIndices;
int fFilteredCount;
private FilterMatcher fFilterMatcher = new DefaultFilterMatcher();
Comparator fComparator;
TableUpdateJob fUpdateJob;
/**
* Label is a private class used for comparing list objects
*/
private static class Label {
/**
* The string portion of the label.
*/
public final String string;
/**
* The image portion of the label.
*/
public final Image image;
/**
* Create a new instance of label.
*
* @param newString
* @param image
*/
public Label(String newString, Image image) {
if (newString == null) {
this.string = Util.ZERO_LENGTH_STRING;
} else {
this.string = newString;
}
this.image = image;
}
/**
* Return whether or not the receiver is the same as label.
*
* @param label
* @return boolean
*/
public boolean equals(Label label) {
if (label == null) {
return false;
}
// If the string portions match (whether null or not), fall
// through and check the image portion.
if (string == null && label.string != null) {
return false;
}
if ((string != null) && (!string.equals(label.string))) {
return false;
}
if (image == null) {
return label.image == null;
}
return image.equals(label.image);
}
}
private final class LabelComparator implements Comparator {
private boolean labelIgnoreCase;
LabelComparator(boolean ignoreCase) {
labelIgnoreCase = ignoreCase;
}
public int compare(Object left, Object right) {
Label leftLabel = (Label) left;
Label rightLabel = (Label) right;
int value;
if (fComparator == null) {
value = labelIgnoreCase ? 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 (fUpdateJob != null) {
fUpdateJob.cancel();
}
}
});
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.
*
* @param filterMatcher
*/
public void setFilterMatcher(FilterMatcher filterMatcher) {
Assert.isNotNull(filterMatcher);
fFilterMatcher = filterMatcher;
}
/**
* Sets a custom comparator for sorting the list.
*
* @param comparator
*/
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 {
// If there is no working update job, or the update job is ready to
// accept selections, set the selection immediately.
if (fUpdateJob == null) {
fList.setSelection(selection);
fList.notifyListeners(SWT.Selection, new Event());
} else {
// There is an update job doing the population of the list, so
// it should update the selection.
fUpdateJob.updateSelection(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;
}
}
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 (fUpdateJob != null) {
fUpdateJob.cancel();
}
fUpdateJob = new TableUpdateJob(fList, fFoldedCount);
fUpdateJob.schedule();
}
/**
* 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;
}
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 class TableUpdateJob extends WorkbenchJob {
final Table fTable;
final int fCount;
private int currentIndex = 0;
/*
* Programmatic selections requested while this job was running.
*/
int[] indicesToSelect;
private boolean readyForSelection = false;
/**
* Create a new instance of a job used to update the table.
*
* @param table
* @param count
* The number of items to update per running.
*/
public TableUpdateJob(Table table, int count) {
super(WorkbenchMessages.FilteredList_UpdateJobName);
setSystem(true);
fTable = table;
fCount = count;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.ui.progress.UIJob#runInUIThread(org.eclipse.core.runtime.IProgressMonitor)
*/
public IStatus runInUIThread(IProgressMonitor monitor) {
if (fTable.isDisposed()) {
return Status.CANCEL_STATUS;
}
int itemCount = fTable.getItemCount();
// Remove excess items
if (fCount < itemCount) {
fTable.setRedraw(false);
fTable.remove(fCount, itemCount - 1);
fTable.setRedraw(true);
itemCount = fTable.getItemCount();
}
// table empty -> no selection
if (fCount == 0) {
fTable.notifyListeners(SWT.Selection, new Event());
return Status.OK_STATUS;
}
// How many we are going to do this time.
int iterations = Math.min(10, fCount - currentIndex);
for (int i = 0; i < iterations; i++) {
if (monitor.isCanceled()) {
return Status.CANCEL_STATUS;
}
final TableItem item = (currentIndex < itemCount) ? fTable
.getItem(currentIndex)
: new TableItem(fTable, SWT.NONE);
final Label label = fLabels[fFilteredIndices[fFoldedIndices[currentIndex]]];
item.setText(label.string);
item.setImage(label.image);
currentIndex++;
}
if (monitor.isCanceled()) {
return Status.CANCEL_STATUS;
}
if (currentIndex < fCount) {
schedule(100);
} else {
if (indicesToSelect == null) {
// Make a default selection in the table if there is none.
// If a selection has already been made, honor it.
// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=112146
if (fCount > 0) {
if (fTable.getSelectionIndices().length == 0) {
defaultSelect();
} else {
// There is a selection, but it likely hasn't changed since the
// job started. Force a selection notification, since the
// items represented by the selection have changed.
// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=119456
fTable.notifyListeners(SWT.Selection, new Event());
}
}
} else {
// Set the selection as indicated.
selectAndNotify(indicesToSelect);
}
// This flag signifies that the selection can now be directly
// updated in the widget.
readyForSelection = true;
}
return Status.OK_STATUS;
}
/**
* Update the selection for the supplied indices.
*
* @param indices
*/
void updateSelection(final int[] indices) {
indicesToSelect = indices;
if (readyForSelection) {
selectAndNotify(indices);
}
}
/**
* Select the first element if there is no selection
*/
private void defaultSelect() {
/**
* Reset to the first selection if no index has been queued.
*/
selectAndNotify(new int[] { 0 });
}
/**
* Select the supplied indices and notify any listeners
*
* @param indices
*/
private void selectAndNotify(final int[] indices) {
// It is possible that the table was disposed
// before the update finished. If so then leave
if (fTable.isDisposed()) {
return;
}
fTable.setSelection(indices);
fTable.notifyListeners(SWT.Selection, new Event());
}
}
/**
* 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;
}
/**
* Returns the accessible object for the receiver.
* If this is the first time this object is requested,
* then the object is created and returned.
*
* @return the accessible object
*
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*
* @see Accessible#addAccessibleListener
* @see Accessible#addAccessibleControlListener
*
* @since 3.3
*/
public Accessible getAccessible() {
return fList.getAccessible();
}
}