Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Scholz2014-10-30 19:35:51 +0000
committerLars Vogel2014-11-03 06:59:29 +0000
commit4b92a487836c4e58073e0fad0650d69b7e0c45ea (patch)
tree5b8ff416bdfd3a5d454a6d8e1c854b1134a9f5af
parentd04a5dfd9d252e9202b435f22b5f69d805358150 (diff)
downloadeclipse.platform.ui-4b92a487836c4e58073e0fad0650d69b7e0c45ea.tar.gz
eclipse.platform.ui-4b92a487836c4e58073e0fad0650d69b7e0c45ea.tar.xz
eclipse.platform.ui-4b92a487836c4e58073e0fad0650d69b7e0c45ea.zip
Bug 440366 - Make FilteredTree available for Eclipse 4 RCP application
Change-Id: Ic5b47d7b4b5964a77d0c485069e154d30fe4bf25 Signed-off-by: Simon Scholz <simon.scholz@vogella.com>
-rw-r--r--bundles/org.eclipse.e4.ui.dialogs/META-INF/MANIFEST.MF3
-rw-r--r--bundles/org.eclipse.e4.ui.dialogs/icons/full/dtool16/clear_co.pngbin0 -> 397 bytes
-rw-r--r--bundles/org.eclipse.e4.ui.dialogs/icons/full/etool16/clear_co.pngbin0 -> 463 bytes
-rw-r--r--bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/BasicUIJob.java95
-rw-r--r--bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/FilteredTree.java1198
-rw-r--r--bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/PatternFilter.java359
-rw-r--r--bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/StringMatcher.java495
-rw-r--r--bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/E4DialogMessages.java34
-rw-r--r--bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/messages.properties22
9 files changed, 2206 insertions, 0 deletions
diff --git a/bundles/org.eclipse.e4.ui.dialogs/META-INF/MANIFEST.MF b/bundles/org.eclipse.e4.ui.dialogs/META-INF/MANIFEST.MF
index 07f83d09ef7..d0fc3394afb 100644
--- a/bundles/org.eclipse.e4.ui.dialogs/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.e4.ui.dialogs/META-INF/MANIFEST.MF
@@ -6,3 +6,6 @@ Bundle-Version: 1.0.0.qualifier
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Bundle-Vendor: %providerName
Export-Package: org.eclipse.e4.ui.dialogs;x-internal:=true
+Require-Bundle: org.eclipse.jface;bundle-version="3.11.0",
+ com.ibm.icu,
+ org.eclipse.core.runtime;bundle-version="3.10.0"
diff --git a/bundles/org.eclipse.e4.ui.dialogs/icons/full/dtool16/clear_co.png b/bundles/org.eclipse.e4.ui.dialogs/icons/full/dtool16/clear_co.png
new file mode 100644
index 00000000000..39ed58beb7d
--- /dev/null
+++ b/bundles/org.eclipse.e4.ui.dialogs/icons/full/dtool16/clear_co.png
Binary files differ
diff --git a/bundles/org.eclipse.e4.ui.dialogs/icons/full/etool16/clear_co.png b/bundles/org.eclipse.e4.ui.dialogs/icons/full/etool16/clear_co.png
new file mode 100644
index 00000000000..b09a154a2db
--- /dev/null
+++ b/bundles/org.eclipse.e4.ui.dialogs/icons/full/etool16/clear_co.png
Binary files differ
diff --git a/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/BasicUIJob.java b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/BasicUIJob.java
new file mode 100644
index 00000000000..419f7d9c1da
--- /dev/null
+++ b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/BasicUIJob.java
@@ -0,0 +1,95 @@
+/*******************************************************************************
+ * Copyright (c) 2010 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
+ *******************************************************************************/
+package org.eclipse.e4.ui.dialogs.filteredtree;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * Merge of UIJob and WokbenchJob minus tracking whether the workbench is
+ * running - do not use for long running jobs!
+ */
+public abstract class BasicUIJob extends Job {
+
+ private Display cachedDisplay;
+
+ /**
+ * Create a new instance of the receiver with the supplied name. The display
+ * used will be the one from the workbench if this is available. UIJobs with
+ * this constructor will determine their display at runtime.
+ *
+ * @param name
+ * the job name
+ *
+ */
+ public BasicUIJob(String name, Display display) {
+ super(name);
+ this.cachedDisplay = display;
+ }
+
+ /**
+ * @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor)
+ * Note: this message is marked final. Implementors should use
+ * runInUIThread() instead.
+ */
+ @Override
+ public final IStatus run(final IProgressMonitor monitor) {
+ if (monitor.isCanceled()) {
+ return Status.CANCEL_STATUS;
+ }
+ Display asyncDisplay = (cachedDisplay == null) ? getDisplay()
+ : cachedDisplay;
+ if (asyncDisplay == null || asyncDisplay.isDisposed()) {
+ return Status.CANCEL_STATUS;
+ }
+ asyncDisplay.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ IStatus result = null;
+ try {
+ // As we are in the UI Thread we can
+ // always know what to tell the job.
+ setThread(Thread.currentThread());
+ if (monitor.isCanceled()) {
+ result = Status.CANCEL_STATUS;
+ } else {
+ result = runInUIThread(monitor);
+ }
+ } finally {
+ done(result);
+ }
+ }
+ });
+ return Job.ASYNC_FINISH;
+ }
+
+ /**
+ * Run the job in the UI Thread.
+ *
+ * @param monitor
+ * @return IStatus
+ */
+ public abstract IStatus runInUIThread(IProgressMonitor monitor);
+
+ /**
+ * Returns the display for use by the receiver when running in an asyncExec.
+ * If it is not set then the display set in the workbench is used. If the
+ * display is null the job will not be run.
+ *
+ * @return Display or <code>null</code>.
+ */
+ public Display getDisplay() {
+ return (cachedDisplay != null) ? cachedDisplay : Display.getCurrent();
+ }
+}
diff --git a/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/FilteredTree.java b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/FilteredTree.java
new file mode 100644
index 00000000000..7c2dd738e97
--- /dev/null
+++ b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/FilteredTree.java
@@ -0,0 +1,1198 @@
+/*******************************************************************************
+ * Copyright (c) 2004, 2014 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
+ * Jacek Pospychala - bug 187762
+ * Mohamed Tarief - tarief@eg.ibm.com - IBM - Bug 174481
+ * Lars Vogel <Lars.Vogel@gmail.com> - Bug 440381
+ *******************************************************************************/
+package org.eclipse.e4.ui.dialogs.filteredtree;
+
+import java.net.URL;
+
+import org.eclipse.core.runtime.FileLocator;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.e4.ui.dialogs.textbundles.E4DialogMessages;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.viewers.IContentProvider;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.accessibility.ACC;
+import org.eclipse.swt.accessibility.AccessibleAdapter;
+import org.eclipse.swt.accessibility.AccessibleControlAdapter;
+import org.eclipse.swt.accessibility.AccessibleControlEvent;
+import org.eclipse.swt.accessibility.AccessibleEvent;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.FocusAdapter;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.KeyAdapter;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.TraverseEvent;
+import org.eclipse.swt.events.TraverseListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.FrameworkUtil;
+
+/**
+ * Based on org.eclipse.ui.dialogs.FilteredTree.
+ */
+public class FilteredTree extends Composite {
+
+ /**
+ * The filter text widget to be used by this tree. This value may be
+ * <code>null</code> if there is no filter widget, or if the controls have
+ * not yet been created.
+ */
+ protected Text filterText;
+
+ /**
+ * The control representing the clear button for the filter text entry. This
+ * value may be <code>null</code> if no such button exists, or if the
+ * controls have not yet been created.
+ * <p>
+ * <strong>Note:</strong> As of 3.5, this is not used if the new look is
+ * chosen.
+ * </p>
+ */
+ protected ToolBarManager filterToolBar;
+
+ /**
+ * The control representing the clear button for the filter text entry. This
+ * value may be <code>null</code> if no such button exists, or if the
+ * controls have not yet been created.
+ * <p>
+ * <strong>Note:</strong> This is only used if the new look is chosen.
+ * </p>
+ *
+ * @since 3.5
+ */
+ protected Control clearButtonControl;
+
+ /**
+ * The viewer for the filtered tree. This value should never be
+ * <code>null</code> after the widget creation methods are complete.
+ */
+ protected TreeViewer treeViewer;
+
+ /**
+ * The Composite on which the filter controls are created. This is used to
+ * set the background color of the filter controls to match the surrounding
+ * controls.
+ */
+ protected Composite filterComposite;
+
+ /**
+ * The pattern filter for the tree. This value must not be <code>null</code>
+ * .
+ */
+ private PatternFilter patternFilter;
+
+ /**
+ * The text to initially show in the filter text control.
+ */
+ protected String initialText = ""; //$NON-NLS-1$
+
+ /**
+ * The job used to refresh the tree.
+ */
+ private Job refreshJob;
+
+ /**
+ * The parent composite of the filtered tree.
+ *
+ * @since 3.3
+ */
+ protected Composite parent;
+
+ /**
+ * Whether or not to show the filter controls (text and clear button). The
+ * default is to show these controls. This can be overridden by providing a
+ * setting in the product configuration file. The setting to add to not show
+ * these controls is:
+ *
+ * org.eclipse.ui/SHOW_FILTERED_TEXTS=false
+ */
+ protected boolean showFilterControls;
+
+ /**
+ * @since 3.3
+ */
+ protected Composite treeComposite;
+
+ /**
+ * Image descriptor for enabled clear button.
+ */
+ private static final String CLEAR_ICON = "org.eclipse.ui.internal.dialogs.CLEAR_ICON"; //$NON-NLS-1$
+
+ /**
+ * Image descriptor for disabled clear button.
+ */
+ private static final String DISABLED_CLEAR_ICON = "org.eclipse.ui.internal.dialogs.DCLEAR_ICON"; //$NON-NLS-1$
+
+ /**
+ * Maximum time spent expanding the tree after the filter text has been
+ * updated (this is only used if we were able to at least expand the visible
+ * nodes)
+ */
+ private static final long SOFT_MAX_EXPAND_TIME = 200;
+
+ /**
+ * Get image descriptors for the clear button.
+ */
+ static {
+ Bundle bundle = FrameworkUtil.getBundle(FilteredTree.class);
+ IPath enabledPath = new Path("$nl$/icons/full/etool16/clear_co.gif");
+ URL enabledURL = FileLocator.find(bundle, enabledPath, null);
+ ImageDescriptor enabledDesc = ImageDescriptor.createFromURL(enabledURL);
+ if (enabledDesc != null) {
+ JFaceResources.getImageRegistry().put(CLEAR_ICON, enabledDesc);
+ }
+
+ IPath disabledPath = new Path("$nl$/icons/full/etool16/clear_co.gif");
+ URL disabledURL = FileLocator.find(bundle, disabledPath, null);
+ ImageDescriptor disabledDesc = ImageDescriptor
+ .createFromURL(disabledURL);
+ if (disabledDesc != null) {
+ JFaceResources.getImageRegistry().put(DISABLED_CLEAR_ICON,
+ disabledDesc);
+ }
+ }
+
+ /**
+ * Create a new instance of the receiver.
+ *
+ * @param parent
+ * the parent <code>Composite</code>
+ * @param treeStyle
+ * the style bits for the <code>Tree</code>
+ * @param filter
+ * the filter to be used
+ * @since 3.5
+ */
+ public FilteredTree(Composite parent, int treeStyle, PatternFilter filter) {
+ super(parent, SWT.NONE);
+ this.parent = parent;
+ init(treeStyle, filter);
+ }
+
+ /**
+ * Create a new instance of the receiver.
+ *
+ * @param parent
+ * the parent <code>Composite</code>
+ * @param treeStyle
+ * the style bits for the <code>Tree</code>
+ * @param filter
+ * the filter to be used
+ * @param useNewLook
+ * ignored, look introduced in 3.5 is always used
+ * @since 3.5
+ *
+ * @deprecated use FilteredTree(Composite parent, int treeStyle,
+ * PatternFilter filter)
+ */
+ @Deprecated
+ public FilteredTree(Composite parent, int treeStyle, PatternFilter filter,
+ boolean useNewLook) {
+ this(parent, treeStyle, filter);
+ }
+
+ /**
+ * Create a new instance of the receiver. Subclasses that wish to override
+ * the default creation behavior may use this constructor, but must ensure
+ * that the <code>init(composite, int, PatternFilter)</code> method is
+ * called in the overriding constructor.
+ *
+ * @param parent
+ * the parent <code>Composite</code>
+ * @see #init(int, PatternFilter)
+ *
+ * @since 3.5
+ */
+ protected FilteredTree(Composite parent) {
+ super(parent, SWT.NONE);
+ this.parent = parent;
+ }
+
+ /**
+ * Create a new instance of the receiver. Subclasses that wish to override
+ * the default creation behavior may use this constructor, but must ensure
+ * that the <code>init(composite, int, PatternFilter)</code> method is
+ * called in the overriding constructor.
+ *
+ * @param parent
+ * the parent <code>Composite</code>
+ * @param useNewLook
+ * ignored, look introduced in 3.5 is always used
+ * @see #init(int, PatternFilter)
+ *
+ * @since 3.5
+ *
+ * @deprecated use FilteredTree(Composite parent) instead
+ */
+ @Deprecated
+ protected FilteredTree(Composite parent, boolean useNewLook) {
+ this(parent);
+ }
+
+ /**
+ * Create the filtered tree.
+ *
+ * @param treeStyle
+ * the style bits for the <code>Tree</code>
+ * @param filter
+ * the filter to be used
+ *
+ * @since 3.3
+ */
+ protected void init(int treeStyle, PatternFilter filter) {
+ patternFilter = filter;
+ showFilterControls = true; // PlatformUI.getPreferenceStore().getBoolean(
+ // IWorkbenchPreferenceConstants.SHOW_FILTERED_TEXTS);
+ createControl(parent, treeStyle);
+ createRefreshJob();
+ setInitialText(E4DialogMessages.FilteredTree_FilterMessage);
+ setFont(parent.getFont());
+ }
+
+ /**
+ * Create the filtered tree's controls. Subclasses should override.
+ *
+ * @param parent
+ * @param treeStyle
+ */
+ protected void createControl(Composite parent, int treeStyle) {
+ GridLayout layout = new GridLayout();
+ layout.marginHeight = 0;
+ layout.marginWidth = 0;
+ setLayout(layout);
+ setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+ if (showFilterControls) {
+ if (useNativeSearchField(parent)) {
+ filterComposite = new Composite(this, SWT.NONE);
+ } else {
+ filterComposite = new Composite(this, SWT.BORDER);
+ filterComposite.setBackground(getDisplay().getSystemColor(
+ SWT.COLOR_LIST_BACKGROUND));
+ }
+ GridLayout filterLayout = new GridLayout(2, false);
+ filterLayout.marginHeight = 0;
+ filterLayout.marginWidth = 0;
+ filterComposite.setLayout(filterLayout);
+ filterComposite.setFont(parent.getFont());
+
+ createFilterControls(filterComposite);
+ filterComposite.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING,
+ true, false));
+ }
+
+ treeComposite = new Composite(this, SWT.NONE);
+ GridLayout treeCompositeLayout = new GridLayout();
+ treeCompositeLayout.marginHeight = 0;
+ treeCompositeLayout.marginWidth = 0;
+ treeComposite.setLayout(treeCompositeLayout);
+ GridData data = new GridData(SWT.FILL, SWT.FILL, true, true);
+ treeComposite.setLayoutData(data);
+ createTreeControl(treeComposite, treeStyle);
+ }
+
+ private static Boolean useNativeSearchField;
+
+ private static boolean useNativeSearchField(Composite composite) {
+ if (useNativeSearchField == null) {
+ useNativeSearchField = Boolean.FALSE;
+ Text testText = null;
+ try {
+ testText = new Text(composite, SWT.SEARCH | SWT.ICON_CANCEL);
+ useNativeSearchField = new Boolean(
+ (testText.getStyle() & SWT.ICON_CANCEL) != 0);
+ } finally {
+ if (testText != null) {
+ testText.dispose();
+ }
+ }
+
+ }
+ return useNativeSearchField.booleanValue();
+ }
+
+ /**
+ * Create the filter controls. By default, a text and corresponding tool bar
+ * button that clears the contents of the text is created. Subclasses may
+ * override.
+ *
+ * @param parent
+ * parent <code>Composite</code> of the filter controls
+ * @return the <code>Composite</code> that contains the filter controls
+ */
+ protected Composite createFilterControls(Composite parent) {
+ createFilterText(parent);
+ createClearText(parent);
+ if (clearButtonControl != null) {
+ // initially there is no text to clear
+ clearButtonControl.setVisible(false);
+ }
+ if (filterToolBar != null) {
+ filterToolBar.update(false);
+ // initially there is no text to clear
+ filterToolBar.getControl().setVisible(false);
+ }
+ return parent;
+ }
+
+ /**
+ * Creates and set up the tree and tree viewer. This method calls
+ * {@link #doCreateTreeViewer(Composite, int)} to create the tree viewer.
+ * Subclasses should override {@link #doCreateTreeViewer(Composite, int)}
+ * instead of overriding this method.
+ *
+ * @param parent
+ * parent <code>Composite</code>
+ * @param style
+ * SWT style bits used to create the tree
+ * @return the tree
+ */
+ protected Control createTreeControl(Composite parent, int style) {
+ treeViewer = doCreateTreeViewer(parent, style);
+ GridData data = new GridData(SWT.FILL, SWT.FILL, true, true);
+ treeViewer.getControl().setLayoutData(data);
+ treeViewer.getControl().addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ refreshJob.cancel();
+ }
+ });
+ if (treeViewer instanceof NotifyingTreeViewer) {
+ patternFilter.setUseCache(true);
+ }
+ treeViewer.addFilter(patternFilter);
+ return treeViewer.getControl();
+ }
+
+ /**
+ * Creates the tree viewer. Subclasses may override.
+ *
+ * @param parent
+ * the parent composite
+ * @param style
+ * SWT style bits used to create the tree viewer
+ * @return the tree viewer
+ *
+ * @since 3.3
+ */
+ protected TreeViewer doCreateTreeViewer(Composite parent, int style) {
+ return new NotifyingTreeViewer(parent, style);
+ }
+
+ /**
+ * Return the first item in the tree that matches the filter pattern.
+ *
+ * @param items
+ * @return the first matching TreeItem
+ */
+ private TreeItem getFirstMatchingItem(TreeItem[] items) {
+ for (TreeItem item : items) {
+ if (patternFilter.isLeafMatch(treeViewer, item.getData())
+ && patternFilter.isElementSelectable(item.getData())) {
+ return item;
+ }
+ TreeItem treeItem = getFirstMatchingItem(item.getItems());
+ if (treeItem != null) {
+ return treeItem;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Create the refresh job for the receiver.
+ *
+ */
+ private void createRefreshJob() {
+ refreshJob = doCreateRefreshJob();
+ refreshJob.setSystem(true);
+ }
+
+ /**
+ * Creates a workbench job that will refresh the tree based on the current
+ * filter text. Subclasses may override.
+ *
+ * @return a workbench job that can be scheduled to refresh the tree
+ *
+ * @since 3.4
+ */
+ protected BasicUIJob doCreateRefreshJob() {
+ return new BasicUIJob("Refresh Filter", parent.getDisplay()) {//$NON-NLS-1$
+ @Override
+ public IStatus runInUIThread(IProgressMonitor monitor) {
+ if (treeViewer.getControl().isDisposed()) {
+ return Status.CANCEL_STATUS;
+ }
+
+ String text = getFilterString();
+ if (text == null) {
+ return Status.OK_STATUS;
+ }
+
+ boolean initial = initialText != null
+ && initialText.equals(text);
+ if (initial) {
+ patternFilter.setPattern(null);
+ } else if (text != null) {
+ patternFilter.setPattern(text);
+ }
+
+ Control redrawFalseControl = treeComposite != null ? treeComposite
+ : treeViewer.getControl();
+ try {
+ // don't want the user to see updates that will be made to
+ // the tree
+ // we are setting redraw(false) on the composite to avoid
+ // dancing scrollbar
+ redrawFalseControl.setRedraw(false);
+ if (!narrowingDown) {
+ // collapse all
+ TreeItem[] is = treeViewer.getTree().getItems();
+ for (TreeItem item : is) {
+ if (item.getExpanded()) {
+ treeViewer.setExpandedState(item.getData(),
+ false);
+ }
+ }
+ }
+ treeViewer.refresh(true);
+
+ if (text.length() > 0 && !initial) {
+ /*
+ * Expand elements one at a time. After each is
+ * expanded, check to see if the filter text has been
+ * modified. If it has, then cancel the refresh job so
+ * the user doesn't have to endure expansion of all the
+ * nodes.
+ */
+ TreeItem[] items = getViewer().getTree().getItems();
+ int treeHeight = getViewer().getTree().getBounds().height;
+ int numVisibleItems = treeHeight
+ / getViewer().getTree().getItemHeight();
+ long stopTime = SOFT_MAX_EXPAND_TIME
+ + System.currentTimeMillis();
+ boolean cancel = false;
+ if (items.length > 0
+ && recursiveExpand(items, monitor, stopTime,
+ new int[] { numVisibleItems })) {
+ cancel = true;
+ }
+
+ // enabled toolbar - there is text to clear
+ // and the list is currently being filtered
+ updateToolbar(true);
+
+ if (cancel) {
+ return Status.CANCEL_STATUS;
+ }
+ } else {
+ // disabled toolbar - there is no text to clear
+ // and the list is currently not filtered
+ updateToolbar(false);
+ }
+ } finally {
+ // done updating the tree - set redraw back to true
+ TreeItem[] items = getViewer().getTree().getItems();
+ if (items.length > 0
+ && getViewer().getTree().getSelectionCount() == 0) {
+ treeViewer.getTree().setTopItem(items[0]);
+ }
+ redrawFalseControl.setRedraw(true);
+ }
+ return Status.OK_STATUS;
+ }
+
+ /**
+ * Returns true if the job should be canceled (because of timeout or
+ * actual cancellation).
+ *
+ * @param items
+ * @param monitor
+ * @param cancelTime
+ * @param numItemsLeft
+ * @return true if canceled
+ */
+ private boolean recursiveExpand(TreeItem[] items,
+ IProgressMonitor monitor, long cancelTime,
+ int[] numItemsLeft) {
+ boolean canceled = false;
+ for (int i = 0; !canceled && i < items.length; i++) {
+ TreeItem item = items[i];
+ boolean visible = numItemsLeft[0]-- >= 0;
+ if (monitor.isCanceled()
+ || (!visible && System.currentTimeMillis() > cancelTime)) {
+ canceled = true;
+ } else {
+ Object itemData = item.getData();
+ if (itemData != null) {
+ if (!item.getExpanded()) {
+ // do the expansion through the viewer so that
+ // it can refresh children appropriately.
+ treeViewer.setExpandedState(itemData, true);
+ }
+ TreeItem[] children = item.getItems();
+ if (items.length > 0) {
+ canceled = recursiveExpand(children, monitor,
+ cancelTime, numItemsLeft);
+ }
+ }
+ }
+ }
+ return canceled;
+ }
+
+ };
+ }
+
+ protected void updateToolbar(boolean visible) {
+ if (clearButtonControl != null) {
+ clearButtonControl.setVisible(visible);
+ }
+ if (filterToolBar != null) {
+ filterToolBar.getControl().setVisible(visible);
+ }
+ }
+
+ /**
+ * Creates the filter text and adds listeners. This method calls
+ * {@link #doCreateFilterText(Composite)} to create the text control.
+ * Subclasses should override {@link #doCreateFilterText(Composite)} instead
+ * of overriding this method.
+ *
+ * @param parent
+ * <code>Composite</code> of the filter text
+ */
+ protected void createFilterText(Composite parent) {
+ filterText = doCreateFilterText(parent);
+ filterText.getAccessible().addAccessibleListener(
+ new AccessibleAdapter() {
+ @Override
+ public void getName(AccessibleEvent e) {
+ String filterTextString = filterText.getText();
+ if (filterTextString.length() == 0
+ || filterTextString.equals(initialText)) {
+ e.result = initialText;
+ } else {
+ e.result = NLS
+ .bind(
+ E4DialogMessages.FilteredTree_AccessibleListenerFiltered,
+ new String[] {
+ filterTextString,
+ String.valueOf(getFilteredItemsCount()) });
+ }
+ }
+
+ /**
+ * Return the number of filtered items
+ *
+ * @return int
+ */
+ private int getFilteredItemsCount() {
+ int total = 0;
+ TreeItem[] items = getViewer().getTree().getItems();
+ for (TreeItem item : items) {
+ total += itemCount(item);
+
+ }
+ return total;
+ }
+
+ /**
+ * Return the count of treeItem and it's children to
+ * infinite depth.
+ *
+ * @param treeItem
+ * @return int
+ */
+ private int itemCount(TreeItem treeItem) {
+ int count = 1;
+ TreeItem[] children = treeItem.getItems();
+ for (TreeItem element : children) {
+ count += itemCount(element);
+
+ }
+ return count;
+ }
+ });
+
+ filterText.addFocusListener(new FocusAdapter() {
+
+ @Override
+ public void focusLost(FocusEvent e) {
+ if (filterText.getText().equals(initialText)) {
+ setFilterText(""); //$NON-NLS-1$
+ textChanged();
+ }
+ }
+ });
+
+ filterText.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseDown(MouseEvent e) {
+ if (filterText.getText().equals(initialText)) {
+ // XXX: We cannot call clearText() due to
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=260664
+ setFilterText(""); //$NON-NLS-1$
+ textChanged();
+ }
+ }
+ });
+
+ filterText.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ // on a CR we want to transfer focus to the list
+ boolean hasItems = getViewer().getTree().getItemCount() > 0;
+ if (hasItems && e.keyCode == SWT.ARROW_DOWN) {
+ treeViewer.getTree().setFocus();
+ return;
+ }
+ }
+ });
+
+ // enter key set focus to tree
+ filterText.addTraverseListener(new TraverseListener() {
+ @Override
+ public void keyTraversed(TraverseEvent e) {
+ if (e.detail == SWT.TRAVERSE_RETURN) {
+ e.doit = false;
+ if (getViewer().getTree().getItemCount() == 0) {
+ Display.getCurrent().beep();
+ } else {
+ // if the initial filter text hasn't changed, do not try
+ // to match
+ boolean hasFocus = getViewer().getTree().setFocus();
+ boolean textChanged = !getInitialText().equals(
+ filterText.getText().trim());
+ if (hasFocus && textChanged
+ && filterText.getText().trim().length() > 0) {
+ Tree tree = getViewer().getTree();
+ TreeItem item;
+ if (tree.getSelectionCount() > 0) {
+ item = getFirstMatchingItem(tree.getSelection());
+ } else {
+ item = getFirstMatchingItem(tree.getItems());
+ }
+ if (item != null) {
+ tree.setSelection(new TreeItem[] { item });
+ ISelection sel = getViewer().getSelection();
+ getViewer().setSelection(sel, true);
+ }
+ }
+ }
+ }
+ }
+ });
+
+ filterText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ textChanged();
+ }
+ });
+
+ // if we're using a field with built in cancel we need to listen for
+ // default selection changes (which tell us the cancel button has been
+ // pressed)
+ if ((filterText.getStyle() & SWT.ICON_CANCEL) != 0) {
+ filterText.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ if (e.detail == SWT.ICON_CANCEL) {
+ clearText();
+ }
+ }
+ });
+ }
+
+ GridData gridData = new GridData(SWT.FILL, SWT.CENTER, true, false);
+ // if the text widget supported cancel then it will have it's own
+ // integrated button. We can take all of the space.
+ if ((filterText.getStyle() & SWT.ICON_CANCEL) != 0) {
+ gridData.horizontalSpan = 2;
+ }
+ filterText.setLayoutData(gridData);
+ }
+
+ /**
+ * Creates the text control for entering the filter text. Subclasses may
+ * override.
+ *
+ * @param parent
+ * the parent composite
+ * @return the text widget
+ *
+ * @since 3.3
+ */
+ protected Text doCreateFilterText(Composite parent) {
+ if (useNativeSearchField(parent)) {
+ return new Text(parent, SWT.SINGLE | SWT.BORDER | SWT.SEARCH
+ | SWT.ICON_CANCEL);
+ }
+ return new Text(parent, SWT.SINGLE);
+ }
+
+ private String previousFilterText;
+
+ private boolean narrowingDown;
+
+ /**
+ * Update the receiver after the text has changed.
+ */
+ protected void textChanged() {
+ narrowingDown = previousFilterText == null
+ || previousFilterText
+ .equals(E4DialogMessages.FilteredTree_FilterMessage)
+ || getFilterString().startsWith(previousFilterText);
+ previousFilterText = getFilterString();
+ // cancel currently running job first, to prevent unnecessary redraw
+ refreshJob.cancel();
+ refreshJob.schedule(getRefreshJobDelay());
+ }
+
+ /**
+ * Return the time delay that should be used when scheduling the filter
+ * refresh job. Subclasses may override.
+ *
+ * @return a time delay in milliseconds before the job should run
+ *
+ * @since 3.5
+ */
+ protected long getRefreshJobDelay() {
+ return 200;
+ }
+
+ /**
+ * Set the background for the widgets that support the filter text area.
+ *
+ * @param background
+ * background <code>Color</code> to set
+ */
+ @Override
+ public void setBackground(Color background) {
+ super.setBackground(background);
+ if (filterComposite != null && (useNativeSearchField(filterComposite))) {
+ filterComposite.setBackground(background);
+ }
+ if (filterToolBar != null && filterToolBar.getControl() != null) {
+ filterToolBar.getControl().setBackground(background);
+ }
+ }
+
+
+ /**
+ * Create the button that clears the text.
+ *
+ * @param parent
+ * parent <code>Composite</code> of toolbar button
+ */
+ private void createClearText(Composite parent) {
+ // only create the button if the text widget doesn't support one
+ // natively
+ if ((filterText.getStyle() & SWT.ICON_CANCEL) == 0) {
+ final Image inactiveImage = JFaceResources.getImageRegistry()
+ .getDescriptor(DISABLED_CLEAR_ICON).createImage();
+ final Image activeImage = JFaceResources.getImageRegistry()
+ .getDescriptor(CLEAR_ICON).createImage();
+ final Image pressedImage = new Image(getDisplay(), activeImage,
+ SWT.IMAGE_GRAY);
+
+ final Label clearButton = new Label(parent, SWT.NONE);
+ clearButton.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER,
+ false, false));
+ clearButton.setImage(inactiveImage);
+ clearButton.setBackground(parent.getDisplay().getSystemColor(
+ SWT.COLOR_LIST_BACKGROUND));
+ clearButton
+ .setToolTipText(E4DialogMessages.FilteredTree_ClearToolTip);
+ clearButton.addMouseListener(new MouseAdapter() {
+ private MouseMoveListener fMoveListener;
+
+ @Override
+ public void mouseDown(MouseEvent e) {
+ clearButton.setImage(pressedImage);
+ fMoveListener = new MouseMoveListener() {
+ private boolean fMouseInButton = true;
+
+ @Override
+ public void mouseMove(MouseEvent e) {
+ boolean mouseInButton = isMouseInButton(e);
+ if (mouseInButton != fMouseInButton) {
+ fMouseInButton = mouseInButton;
+ clearButton
+ .setImage(mouseInButton ? pressedImage
+ : inactiveImage);
+ }
+ }
+ };
+ clearButton.addMouseMoveListener(fMoveListener);
+ }
+
+ @Override
+ public void mouseUp(MouseEvent e) {
+ if (fMoveListener != null) {
+ clearButton.removeMouseMoveListener(fMoveListener);
+ fMoveListener = null;
+ boolean mouseInButton = isMouseInButton(e);
+ clearButton.setImage(mouseInButton ? activeImage
+ : inactiveImage);
+ if (mouseInButton) {
+ clearText();
+ filterText.setFocus();
+ }
+ }
+ }
+
+ private boolean isMouseInButton(MouseEvent e) {
+ Point buttonSize = clearButton.getSize();
+ return 0 <= e.x && e.x < buttonSize.x && 0 <= e.y
+ && e.y < buttonSize.y;
+ }
+ });
+ clearButton.addMouseTrackListener(new MouseTrackListener() {
+ @Override
+ public void mouseEnter(MouseEvent e) {
+ clearButton.setImage(activeImage);
+ }
+
+ @Override
+ public void mouseExit(MouseEvent e) {
+ clearButton.setImage(inactiveImage);
+ }
+
+ @Override
+ public void mouseHover(MouseEvent e) {
+ }
+ });
+ clearButton.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ inactiveImage.dispose();
+ activeImage.dispose();
+ pressedImage.dispose();
+ }
+ });
+ clearButton.getAccessible().addAccessibleListener(
+ new AccessibleAdapter() {
+ @Override
+ public void getName(AccessibleEvent e) {
+ e.result = E4DialogMessages.FilteredTree_AccessibleListenerClearButton;
+ }
+ });
+ clearButton.getAccessible().addAccessibleControlListener(
+ new AccessibleControlAdapter() {
+ @Override
+ public void getRole(AccessibleControlEvent e) {
+ e.detail = ACC.ROLE_PUSHBUTTON;
+ }
+ });
+ this.clearButtonControl = clearButton;
+ }
+ }
+
+ /**
+ * Clears the text in the filter text widget.
+ */
+ protected void clearText() {
+ setFilterText(""); //$NON-NLS-1$
+ textChanged();
+ }
+
+ /**
+ * Set the text in the filter control.
+ *
+ * @param string
+ */
+ protected void setFilterText(String string) {
+ if (filterText != null) {
+ filterText.setText(string);
+ selectAll();
+ }
+ }
+
+ /**
+ * Returns the pattern filter used by this tree.
+ *
+ * @return The pattern filter; never <code>null</code>.
+ */
+ public final PatternFilter getPatternFilter() {
+ return patternFilter;
+ }
+
+ /**
+ * Get the tree viewer of the receiver.
+ *
+ * @return the tree viewer
+ */
+ public TreeViewer getViewer() {
+ return treeViewer;
+ }
+
+ /**
+ * Get the filter text for the receiver, if it was created. Otherwise return
+ * <code>null</code>.
+ *
+ * @return the filter Text, or null if it was not created
+ */
+ public Text getFilterControl() {
+ return filterText;
+ }
+
+ /**
+ * Convenience method to return the text of the filter control. If the text
+ * widget is not created, then null is returned.
+ *
+ * @return String in the text, or null if the text does not exist
+ */
+ protected String getFilterString() {
+ return filterText != null ? filterText.getText() : null;
+ }
+
+ /**
+ * Set the text that will be shown until the first focus. A default value is
+ * provided, so this method only need be called if overriding the default
+ * initial text is desired.
+ *
+ * @param text
+ * initial text to appear in text field
+ */
+ public void setInitialText(String text) {
+ initialText = text;
+ if (filterText != null) {
+ filterText.setMessage(text);
+ if (filterText.isFocusControl()) {
+ setFilterText(initialText);
+ textChanged();
+ } else {
+ getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (!filterText.isDisposed()
+ && filterText.isFocusControl()) {
+ setFilterText(initialText);
+ textChanged();
+ }
+ }
+ });
+ }
+ } else {
+ setFilterText(initialText);
+ textChanged();
+ }
+ }
+
+ /**
+ * Select all text in the filter text field.
+ *
+ */
+ protected void selectAll() {
+ if (filterText != null) {
+ filterText.selectAll();
+ }
+ }
+
+ /**
+ * Get the initial text for the receiver.
+ *
+ * @return String
+ */
+ protected String getInitialText() {
+ return initialText;
+ }
+
+ /**
+ * Return a bold font if the given element matches the given pattern.
+ * Clients can opt to call this method from a Viewer's label provider to get
+ * a bold font for which to highlight the given element in the tree.
+ *
+ * @param element
+ * element for which a match should be determined
+ * @param tree
+ * FilteredTree in which the element resides
+ * @param filter
+ * PatternFilter which determines a match
+ *
+ * @return bold font
+ */
+ public static Font getBoldFont(Object element, FilteredTree tree,
+ PatternFilter filter) {
+ String filterText = tree.getFilterString();
+
+ if (filterText == null) {
+ return null;
+ }
+
+ // Do nothing if it's empty string
+ String initialText = tree.getInitialText();
+ if (!filterText.equals("") && !filterText.equals(initialText)) {//$NON-NLS-1$
+ if (tree.getPatternFilter() != filter) {
+ boolean initial = initialText != null
+ && initialText.equals(filterText);
+ if (initial) {
+ filter.setPattern(null);
+ } else if (filterText != null) {
+ filter.setPattern(filterText);
+ }
+ }
+ if (filter.isElementVisible(tree.getViewer(), element)
+ && filter.isLeafMatch(tree.getViewer(), element)) {
+ return JFaceResources.getFontRegistry().getBold(
+ JFaceResources.DIALOG_FONT);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Custom tree viewer subclass that clears the caches in patternFilter on
+ * any change to the tree. See bug 187200.
+ *
+ * @since 3.3
+ *
+ */
+ class NotifyingTreeViewer extends TreeViewer {
+
+ /**
+ * @param parent
+ * @param style
+ */
+ public NotifyingTreeViewer(Composite parent, int style) {
+ super(parent, style);
+ }
+
+ @Override
+ public void add(Object parentElementOrTreePath, Object childElement) {
+ getPatternFilter().clearCaches();
+ super.add(parentElementOrTreePath, childElement);
+ }
+
+ @Override
+ public void add(Object parentElementOrTreePath, Object[] childElements) {
+ getPatternFilter().clearCaches();
+ super.add(parentElementOrTreePath, childElements);
+ }
+
+ @Override
+ protected void inputChanged(Object input, Object oldInput) {
+ getPatternFilter().clearCaches();
+ super.inputChanged(input, oldInput);
+ }
+
+ @Override
+ public void insert(Object parentElementOrTreePath, Object element,
+ int position) {
+ getPatternFilter().clearCaches();
+ super.insert(parentElementOrTreePath, element, position);
+ }
+
+ @Override
+ public void refresh() {
+ getPatternFilter().clearCaches();
+ super.refresh();
+ }
+
+ @Override
+ public void refresh(boolean updateLabels) {
+ getPatternFilter().clearCaches();
+ super.refresh(updateLabels);
+ }
+
+ @Override
+ public void refresh(Object element) {
+ getPatternFilter().clearCaches();
+ super.refresh(element);
+ }
+
+ @Override
+ public void refresh(Object element, boolean updateLabels) {
+ getPatternFilter().clearCaches();
+ super.refresh(element, updateLabels);
+ }
+
+ @Override
+ public void remove(Object elementsOrTreePaths) {
+ getPatternFilter().clearCaches();
+ super.remove(elementsOrTreePaths);
+ }
+
+ @Override
+ public void remove(Object parent, Object[] elements) {
+ getPatternFilter().clearCaches();
+ super.remove(parent, elements);
+ }
+
+ @Override
+ public void remove(Object[] elementsOrTreePaths) {
+ getPatternFilter().clearCaches();
+ super.remove(elementsOrTreePaths);
+ }
+
+ @Override
+ public void replace(Object parentElementOrTreePath, int index,
+ Object element) {
+ getPatternFilter().clearCaches();
+ super.replace(parentElementOrTreePath, index, element);
+ }
+
+ @Override
+ public void setChildCount(Object elementOrTreePath, int count) {
+ getPatternFilter().clearCaches();
+ super.setChildCount(elementOrTreePath, count);
+ }
+
+ @Override
+ public void setContentProvider(IContentProvider provider) {
+ getPatternFilter().clearCaches();
+ super.setContentProvider(provider);
+ }
+
+ @Override
+ public void setHasChildren(Object elementOrTreePath, boolean hasChildren) {
+ getPatternFilter().clearCaches();
+ super.setHasChildren(elementOrTreePath, hasChildren);
+ }
+
+ }
+
+}
diff --git a/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/PatternFilter.java b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/PatternFilter.java
new file mode 100644
index 00000000000..3bc43ad76b3
--- /dev/null
+++ b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/PatternFilter.java
@@ -0,0 +1,359 @@
+/*******************************************************************************
+ * Copyright (c) 2004, 2014 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
+ *******************************************************************************/
+package org.eclipse.e4.ui.dialogs.filteredtree;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jface.viewers.AbstractTreeViewer;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.StructuredViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerFilter;
+
+import com.ibm.icu.text.BreakIterator;
+
+/**
+ * Based on org.eclipse.ui.dialogs.PatternFilter.
+ */
+public class PatternFilter extends ViewerFilter {
+ /*
+ * Cache of filtered elements in the tree
+ */
+ private Map cache = new HashMap();
+
+ /*
+ * Maps parent elements to TRUE or FALSE
+ */
+ private Map foundAnyCache = new HashMap();
+
+ private boolean useCache = false;
+
+ /**
+ * Whether to include a leading wildcard for all provided patterns. A
+ * trailing wildcard is always included.
+ */
+ private boolean includeLeadingWildcard = false;
+
+ /**
+ * The string pattern matcher used for this pattern filter.
+ */
+ private StringMatcher matcher;
+
+ private boolean useEarlyReturnIfMatcherIsNull = true;
+
+ private static Object[] EMPTY = new Object[0];
+
+ @Override
+ public final Object[] filter(Viewer viewer, Object parent, Object[] elements) {
+ // we don't want to optimize if we've extended the filter ... this
+ // needs to be addressed in 3.4
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=186404
+ if (matcher == null && useEarlyReturnIfMatcherIsNull) {
+ return elements;
+ }
+
+ if (!useCache) {
+ return super.filter(viewer, parent, elements);
+ }
+
+ Object[] filtered = (Object[]) cache.get(parent);
+ if (filtered == null) {
+ Boolean foundAny = (Boolean) foundAnyCache.get(parent);
+ if (foundAny != null && !foundAny.booleanValue()) {
+ filtered = EMPTY;
+ } else {
+ filtered = super.filter(viewer, parent, elements);
+ }
+ cache.put(parent, filtered);
+ }
+ return filtered;
+ }
+
+ /**
+ * Returns true if any of the elements makes it through the filter. This
+ * method uses caching if enabled; the computation is done in
+ * computeAnyVisible.
+ *
+ * @param viewer
+ * @param parent
+ * @param elements
+ * the elements (must not be an empty array)
+ * @return true if any of the elements makes it through the filter.
+ */
+ private boolean isAnyVisible(Viewer viewer, Object parent, Object[] elements) {
+ if (matcher == null) {
+ return true;
+ }
+
+ if (!useCache) {
+ return computeAnyVisible(viewer, elements);
+ }
+
+ Object[] filtered = (Object[]) cache.get(parent);
+ if (filtered != null) {
+ return filtered.length > 0;
+ }
+ Boolean foundAny = (Boolean) foundAnyCache.get(parent);
+ if (foundAny == null) {
+ foundAny = computeAnyVisible(viewer, elements) ? Boolean.TRUE
+ : Boolean.FALSE;
+ foundAnyCache.put(parent, foundAny);
+ }
+ return foundAny.booleanValue();
+ }
+
+ /**
+ * Returns true if any of the elements makes it through the filter.
+ *
+ * @param viewer
+ * the viewer
+ * @param elements
+ * the elements to test
+ * @return <code>true</code> if any of the elements makes it through the
+ * filter
+ */
+ private boolean computeAnyVisible(Viewer viewer, Object[] elements) {
+ boolean elementFound = false;
+ for (int i = 0; i < elements.length && !elementFound; i++) {
+ Object element = elements[i];
+ elementFound = isElementVisible(viewer, element);
+ }
+ return elementFound;
+ }
+
+ @Override
+ public final boolean select(Viewer viewer, Object parentElement,
+ Object element) {
+ return isElementVisible(viewer, element);
+ }
+
+ /**
+ * Sets whether a leading wildcard should be attached to each pattern
+ * string.
+ *
+ * @param includeLeadingWildcard
+ * Whether a leading wildcard should be added.
+ */
+ public final void setIncludeLeadingWildcard(
+ final boolean includeLeadingWildcard) {
+ this.includeLeadingWildcard = includeLeadingWildcard;
+ }
+
+ /**
+ * The pattern string for which this filter should select elements in the
+ * viewer.
+ *
+ * @param patternString
+ */
+ public void setPattern(String patternString) {
+ // these 2 strings allow the PatternFilter to be extended in
+ // 3.3 - https://bugs.eclipse.org/bugs/show_bug.cgi?id=186404
+ if ("org.eclipse.ui.keys.optimization.true".equals(patternString)) { //$NON-NLS-1$
+ useEarlyReturnIfMatcherIsNull = true;
+ return;
+ } else if ("org.eclipse.ui.keys.optimization.false".equals(patternString)) { //$NON-NLS-1$
+ useEarlyReturnIfMatcherIsNull = false;
+ return;
+ }
+ clearCaches();
+ if (patternString == null || patternString.equals("")) { //$NON-NLS-1$
+ matcher = null;
+ } else {
+ String pattern = patternString + "*"; //$NON-NLS-1$
+ if (includeLeadingWildcard) {
+ pattern = "*" + pattern; //$NON-NLS-1$
+ }
+ matcher = new StringMatcher(pattern, true, false);
+ }
+ }
+
+ /**
+ * Clears the caches used for optimizing this filter. Needs to be called
+ * whenever the tree content changes.
+ */
+ /* package */void clearCaches() {
+ cache.clear();
+ foundAnyCache.clear();
+ }
+
+ /**
+ * Answers whether the given String matches the pattern.
+ *
+ * @param string
+ * the String to test
+ *
+ * @return whether the string matches the pattern
+ */
+ private boolean match(String string) {
+ if (matcher == null) {
+ return true;
+ }
+ return matcher.match(string);
+ }
+
+ /**
+ * Answers whether the given element is a valid selection in the filtered
+ * tree. For example, if a tree has items that are categorized, the category
+ * itself may not be a valid selection since it is used merely to organize
+ * the elements.
+ *
+ * @param element
+ * @return true if this element is eligible for automatic selection
+ */
+ public boolean isElementSelectable(Object element) {
+ return element != null;
+ }
+
+ /**
+ * Answers whether the given element in the given viewer matches the filter
+ * pattern. This is a default implementation that will show a leaf element
+ * in the tree based on whether the provided filter text matches the text of
+ * the given element's text, or that of it's children (if the element has
+ * any).
+ *
+ * Subclasses may override this method.
+ *
+ * @param viewer
+ * the tree viewer in which the element resides
+ * @param element
+ * the element in the tree to check for a match
+ *
+ * @return true if the element matches the filter pattern
+ */
+ public boolean isElementVisible(Viewer viewer, Object element) {
+ return isParentMatch(viewer, element) || isLeafMatch(viewer, element);
+ }
+
+ /**
+ * Check if the parent (category) is a match to the filter text. The default
+ * behavior returns true if the element has at least one child element that
+ * is a match with the filter text.
+ *
+ * Subclasses may override this method.
+ *
+ * @param viewer
+ * the viewer that contains the element
+ * @param element
+ * the tree element to check
+ * @return true if the given element has children that matches the filter
+ * text
+ */
+ protected boolean isParentMatch(Viewer viewer, Object element) {
+ Object[] children = ((ITreeContentProvider) ((AbstractTreeViewer) viewer)
+ .getContentProvider()).getChildren(element);
+
+ if ((children != null) && (children.length > 0)) {
+ return isAnyVisible(viewer, element, children);
+ }
+ return false;
+ }
+
+ /**
+ * Check if the current (leaf) element is a match with the filter text. The
+ * default behavior checks that the label of the element is a match.
+ *
+ * Subclasses should override this method.
+ *
+ * @param viewer
+ * the viewer that contains the element
+ * @param element
+ * the tree element to check
+ * @return true if the given element's label matches the filter text
+ */
+ protected boolean isLeafMatch(Viewer viewer, Object element) {
+ String labelText = ((ILabelProvider) ((StructuredViewer) viewer)
+ .getLabelProvider()).getText(element);
+
+ if (labelText == null) {
+ return false;
+ }
+ return wordMatches(labelText);
+ }
+
+ /**
+ * Take the given filter text and break it down into words using a
+ * BreakIterator.
+ *
+ * @param text
+ * @return an array of words
+ */
+ private String[] getWords(String text) {
+ List words = new ArrayList();
+ // Break the text up into words, separating based on whitespace and
+ // common punctuation.
+ // Previously used String.split(..., "\\W"), where "\W" is a regular
+ // expression (see the Javadoc for class Pattern).
+ // Need to avoid both String.split and regular expressions, in order to
+ // compile against JCL Foundation (bug 80053).
+ // Also need to do this in an NL-sensitive way. The use of BreakIterator
+ // was suggested in bug 90579.
+ BreakIterator iter = BreakIterator.getWordInstance();
+ iter.setText(text);
+ int i = iter.first();
+ while (i != java.text.BreakIterator.DONE && i < text.length()) {
+ int j = iter.following(i);
+ if (j == java.text.BreakIterator.DONE) {
+ j = text.length();
+ }
+ // match the word
+ if (Character.isLetterOrDigit(text.charAt(i))) {
+ String word = text.substring(i, j);
+ words.add(word);
+ }
+ i = j;
+ }
+ return (String[]) words.toArray(new String[words.size()]);
+ }
+
+ /**
+ * Return whether or not if any of the words in text satisfy the match
+ * critera.
+ *
+ * @param text
+ * the text to match
+ * @return boolean <code>true</code> if one of the words in text satisifes
+ * the match criteria.
+ */
+ protected boolean wordMatches(String text) {
+ if (text == null) {
+ return false;
+ }
+
+ // If the whole text matches we are all set
+ if (match(text)) {
+ return true;
+ }
+
+ // Otherwise check if any of the words of the text matches
+ String[] words = getWords(text);
+ for (String word : words) {
+ if (match(word)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Can be called by the filtered tree to turn on caching.
+ *
+ * @param useCache
+ * The useCache to set.
+ */
+ void setUseCache(boolean useCache) {
+ this.useCache = useCache;
+ }
+}
diff --git a/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/StringMatcher.java b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/StringMatcher.java
new file mode 100644
index 00000000000..89919a69ef5
--- /dev/null
+++ b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/StringMatcher.java
@@ -0,0 +1,495 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2014 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
+ *******************************************************************************/
+package org.eclipse.e4.ui.dialogs.filteredtree;
+
+import java.util.Vector;
+
+/**
+ * Based on org.eclipse.ui.internal.misc.StringMatcher.
+ */
+public class StringMatcher {
+ protected String fPattern;
+
+ protected int fLength; // pattern length
+
+ protected boolean fIgnoreWildCards;
+
+ protected boolean fIgnoreCase;
+
+ protected boolean fHasLeadingStar;
+
+ protected boolean fHasTrailingStar;
+
+ protected String fSegments[]; // the given pattern is split into * separated
+ // segments
+
+ /* boundary value beyond which we don't need to search in the text */
+ protected int fBound = 0;
+
+ protected static final char fSingleWildCard = '\u0000';
+
+ public static class Position {
+ int start; // inclusive
+
+ int end; // exclusive
+
+ public Position(int start, int end) {
+ this.start = start;
+ this.end = end;
+ }
+
+ public int getStart() {
+ return start;
+ }
+
+ public int getEnd() {
+ return end;
+ }
+ }
+
+ /**
+ * StringMatcher constructor takes in a String object that is a simple
+ * pattern which may contain '*' for 0 and many characters and '?' for
+ * exactly one character.
+ *
+ * Literal '*' and '?' characters must be escaped in the pattern e.g.,
+ * "\*" means literal "*", etc.
+ *
+ * Escaping any other character (including the escape character itself),
+ * just results in that character in the pattern. e.g., "\a" means "a" and
+ * "\\" means "\"
+ *
+ * If invoking the StringMatcher with string literals in Java, don't forget
+ * escape characters are represented by "\\".
+ *
+ * @param pattern
+ * the pattern to match text against
+ * @param ignoreCase
+ * if true, case is ignored
+ * @param ignoreWildCards
+ * if true, wild cards and their escape sequences are ignored
+ * (everything is taken literally).
+ */
+ public StringMatcher(String pattern, boolean ignoreCase,
+ boolean ignoreWildCards) {
+ if (pattern == null) {
+ throw new IllegalArgumentException();
+ }
+ fIgnoreCase = ignoreCase;
+ fIgnoreWildCards = ignoreWildCards;
+ fPattern = pattern;
+ fLength = pattern.length();
+
+ if (fIgnoreWildCards) {
+ parseNoWildCards();
+ } else {
+ parseWildCards();
+ }
+ }
+
+ /**
+ * Find the first occurrence of the pattern between <code>start</code
+ * )(inclusive) and <code>end</code>(exclusive).
+ *
+ * @param text
+ * the String object to search in
+ * @param start
+ * the starting index of the search range, inclusive
+ * @param end
+ * the ending index of the search range, exclusive
+ * @return an <code>StringMatcher.Position</code> object that keeps the
+ * starting (inclusive) and ending positions (exclusive) of the
+ * first occurrence of the pattern in the specified range of the
+ * text; return null if not found or subtext is empty (start==end).
+ * A pair of zeros is returned if pattern is empty string Note that
+ * for pattern like "*abc*" with leading and trailing stars,
+ * position of "abc" is returned. For a pattern like"*??*" in text
+ * "abcdf", (1,3) is returned
+ */
+ public StringMatcher.Position find(String text, int start, int end) {
+ if (text == null) {
+ throw new IllegalArgumentException();
+ }
+
+ int tlen = text.length();
+ if (start < 0) {
+ start = 0;
+ }
+ if (end > tlen) {
+ end = tlen;
+ }
+ if (end < 0 || start >= end) {
+ return null;
+ }
+ if (fLength == 0) {
+ return new Position(start, start);
+ }
+ if (fIgnoreWildCards) {
+ int x = posIn(text, start, end);
+ if (x < 0) {
+ return null;
+ }
+ return new Position(x, x + fLength);
+ }
+
+ int segCount = fSegments.length;
+ if (segCount == 0) {
+ return new Position(start, end);
+ }
+
+ int curPos = start;
+ int matchStart = -1;
+ int i;
+ for (i = 0; i < segCount && curPos < end; ++i) {
+ String current = fSegments[i];
+ int nextMatch = regExpPosIn(text, curPos, end, current);
+ if (nextMatch < 0) {
+ return null;
+ }
+ if (i == 0) {
+ matchStart = nextMatch;
+ }
+ curPos = nextMatch + current.length();
+ }
+ if (i < segCount) {
+ return null;
+ }
+ return new Position(matchStart, curPos);
+ }
+
+ /**
+ * match the given <code>text</code> with the pattern
+ *
+ * @return true if matched otherwise false
+ * @param text
+ * a String object
+ */
+ public boolean match(String text) {
+ if (text == null) {
+ return false;
+ }
+ return match(text, 0, text.length());
+ }
+
+ /**
+ * Given the starting (inclusive) and the ending (exclusive) positions in
+ * the <code>text</code>, determine if the given substring matches with
+ * aPattern
+ *
+ * @return true if the specified portion of the text matches the pattern
+ * @param text
+ * a String object that contains the substring to match
+ * @param start
+ * marks the starting position (inclusive) of the substring
+ * @param end
+ * marks the ending index (exclusive) of the substring
+ */
+ public boolean match(String text, int start, int end) {
+ if (null == text) {
+ throw new IllegalArgumentException();
+ }
+
+ if (start > end) {
+ return false;
+ }
+
+ if (fIgnoreWildCards) {
+ return (end - start == fLength)
+ && fPattern.regionMatches(fIgnoreCase, 0, text, start,
+ fLength);
+ }
+ int segCount = fSegments.length;
+ if (segCount == 0 && (fHasLeadingStar || fHasTrailingStar)) {
+ return true;
+ }
+ if (start == end) {
+ return fLength == 0;
+ }
+ if (fLength == 0) {
+ return start == end;
+ }
+
+ int tlen = text.length();
+ if (start < 0) {
+ start = 0;
+ }
+ if (end > tlen) {
+ end = tlen;
+ }
+
+ int tCurPos = start;
+ int bound = end - fBound;
+ if (bound < 0) {
+ return false;
+ }
+ int i = 0;
+ String current = fSegments[i];
+ int segLength = current.length();
+
+ /* process first segment */
+ if (!fHasLeadingStar) {
+ if (!regExpRegionMatches(text, start, current, 0, segLength)) {
+ return false;
+ }
+ ++i;
+ tCurPos = tCurPos + segLength;
+ }
+ if ((fSegments.length == 1) && (!fHasLeadingStar)
+ && (!fHasTrailingStar)) {
+ // only one segment to match, no wildcards specified
+ return tCurPos == end;
+ }
+ /* process middle segments */
+ while (i < segCount) {
+ current = fSegments[i];
+ int currentMatch;
+ int k = current.indexOf(fSingleWildCard);
+ if (k < 0) {
+ currentMatch = textPosIn(text, tCurPos, end, current);
+ if (currentMatch < 0) {
+ return false;
+ }
+ } else {
+ currentMatch = regExpPosIn(text, tCurPos, end, current);
+ if (currentMatch < 0) {
+ return false;
+ }
+ }
+ tCurPos = currentMatch + current.length();
+ i++;
+ }
+
+ /* process final segment */
+ if (!fHasTrailingStar && tCurPos != end) {
+ int clen = current.length();
+ return regExpRegionMatches(text, end - clen, current, 0, clen);
+ }
+ return i == segCount;
+ }
+
+ /**
+ * This method parses the given pattern into segments seperated by wildcard
+ * '*' characters. Since wildcards are not being used in this case, the
+ * pattern consists of a single segment.
+ */
+ private void parseNoWildCards() {
+ fSegments = new String[1];
+ fSegments[0] = fPattern;
+ fBound = fLength;
+ }
+
+ /**
+ * Parses the given pattern into segments seperated by wildcard '*'
+ * characters.
+ *
+ * @param p
+ * , a String object that is a simple regular expression with '*'
+ * and/or '?'
+ */
+ private void parseWildCards() {
+ if (fPattern.startsWith("*")) { //$NON-NLS-1$
+ fHasLeadingStar = true;
+ }
+ if (fPattern.endsWith("*")) {//$NON-NLS-1$
+ /* make sure it's not an escaped wildcard */
+ if (fLength > 1 && fPattern.charAt(fLength - 2) != '\\') {
+ fHasTrailingStar = true;
+ }
+ }
+
+ Vector temp = new Vector();
+
+ int pos = 0;
+ StringBuffer buf = new StringBuffer();
+ while (pos < fLength) {
+ char c = fPattern.charAt(pos++);
+ switch (c) {
+ case '\\':
+ if (pos >= fLength) {
+ buf.append(c);
+ } else {
+ char next = fPattern.charAt(pos++);
+ /* if it's an escape sequence */
+ if (next == '*' || next == '?' || next == '\\') {
+ buf.append(next);
+ } else {
+ /* not an escape sequence, just insert literally */
+ buf.append(c);
+ buf.append(next);
+ }
+ }
+ break;
+ case '*':
+ if (buf.length() > 0) {
+ /* new segment */
+ temp.addElement(buf.toString());
+ fBound += buf.length();
+ buf.setLength(0);
+ }
+ break;
+ case '?':
+ /* append special character representing single match wildcard */
+ buf.append(fSingleWildCard);
+ break;
+ default:
+ buf.append(c);
+ }
+ }
+
+ /* add last buffer to segment list */
+ if (buf.length() > 0) {
+ temp.addElement(buf.toString());
+ fBound += buf.length();
+ }
+
+ fSegments = new String[temp.size()];
+ temp.copyInto(fSegments);
+ }
+
+ /**
+ * @param text
+ * a string which contains no wildcard
+ * @param start
+ * the starting index in the text for search, inclusive
+ * @param end
+ * the stopping point of search, exclusive
+ * @return the starting index in the text of the pattern , or -1 if not
+ * found
+ */
+ protected int posIn(String text, int start, int end) {// no wild card in
+ // pattern
+ int max = end - fLength;
+
+ if (!fIgnoreCase) {
+ int i = text.indexOf(fPattern, start);
+ if (i == -1 || i > max) {
+ return -1;
+ }
+ return i;
+ }
+
+ for (int i = start; i <= max; ++i) {
+ if (text.regionMatches(true, i, fPattern, 0, fLength)) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * @param text
+ * a simple regular expression that may only contain '?'(s)
+ * @param start
+ * the starting index in the text for search, inclusive
+ * @param end
+ * the stopping point of search, exclusive
+ * @param p
+ * a simple regular expression that may contains '?'
+ * @return the starting index in the text of the pattern , or -1 if not
+ * found
+ */
+ protected int regExpPosIn(String text, int start, int end, String p) {
+ int plen = p.length();
+
+ int max = end - plen;
+ for (int i = start; i <= max; ++i) {
+ if (regExpRegionMatches(text, i, p, 0, plen)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ *
+ * @return boolean
+ * @param text
+ * a String to match
+ * @param start
+ * int that indicates the starting index of match, inclusive
+ * @param end
+ * </code> int that indicates the ending index of match,
+ * exclusive
+ * @param p
+ * String, String, a simple regular expression that may contain
+ * '?'
+ * @param ignoreCase
+ * boolean indicating wether code>p</code> is case sensitive
+ */
+ protected boolean regExpRegionMatches(String text, int tStart, String p,
+ int pStart, int plen) {
+ while (plen-- > 0) {
+ char tchar = text.charAt(tStart++);
+ char pchar = p.charAt(pStart++);
+
+ /* process wild cards */
+ if (!fIgnoreWildCards) {
+ /* skip single wild cards */
+ if (pchar == fSingleWildCard) {
+ continue;
+ }
+ }
+ if (pchar == tchar) {
+ continue;
+ }
+ if (fIgnoreCase) {
+ if (Character.toUpperCase(tchar) == Character
+ .toUpperCase(pchar)) {
+ continue;
+ }
+ // comparing after converting to upper case doesn't handle all
+ // cases;
+ // also compare after converting to lower case
+ if (Character.toLowerCase(tchar) == Character
+ .toLowerCase(pchar)) {
+ continue;
+ }
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @param text
+ * the string to match
+ * @param start
+ * the starting index in the text for search, inclusive
+ * @param end
+ * the stopping point of search, exclusive
+ * @param p
+ * a pattern string that has no wildcard
+ * @return the starting index in the text of the pattern , or -1 if not
+ * found
+ */
+ protected int textPosIn(String text, int start, int end, String p) {
+
+ int plen = p.length();
+ int max = end - plen;
+
+ if (!fIgnoreCase) {
+ int i = text.indexOf(p, start);
+ if (i == -1 || i > max) {
+ return -1;
+ }
+ return i;
+ }
+
+ for (int i = start; i <= max; ++i) {
+ if (text.regionMatches(true, i, p, 0, plen)) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+}
diff --git a/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/E4DialogMessages.java b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/E4DialogMessages.java
new file mode 100644
index 00000000000..7b39e589597
--- /dev/null
+++ b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/E4DialogMessages.java
@@ -0,0 +1,34 @@
+/*******************************************************************************
+ * Copyright (c) 2014 vogella GmbH 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:
+ * Simon Scholz <simon.scholz@vogella.com> - Initial API and implementation based on WorkbenchSWTMessages
+ *******************************************************************************/
+package org.eclipse.e4.ui.dialogs.textbundles;
+
+import org.eclipse.osgi.util.NLS;
+
+/**
+ * Based on org.eclipse.ui.internal.WorkbenchMessages
+ */
+public class E4DialogMessages extends NLS {
+ private static final String BUNDLE_NAME = "org.eclipse.e4.ui.dialogs.textbundles.messages";//$NON-NLS-1$
+
+ public static String FilteredTree_AccessibleListenerClearButton;
+ public static String FilteredTree_ClearToolTip;
+ public static String FilteredTree_FilterMessage;
+ public static String FilteredTree_AccessibleListenerFiltered;
+
+ static {
+ // load message values from bundle file
+ reloadMessages();
+ }
+
+ public static void reloadMessages() {
+ NLS.initializeMessages(BUNDLE_NAME, E4DialogMessages.class);
+ }
+}
diff --git a/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/messages.properties b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/messages.properties
new file mode 100644
index 00000000000..12a504e6ed7
--- /dev/null
+++ b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/messages.properties
@@ -0,0 +1,22 @@
+###############################################################################
+# Copyright (c) 2000, 2010 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 57087
+# - Fix for Bug 138034 [Preferences] Label decorations page - extra space
+# - Fix for Bug 128529
+# Semion Chichelnitsky (semion@il.ibm.com) - bug 278064
+###############################################################################
+
+# Based on /org.eclipse.ui.workbench/Eclipse UI/org/eclipse/ui/internal/messages.properties
+
+FilteredTree_AccessibleListenerClearButton=Clear filter field
+FilteredTree_ClearToolTip=Clear
+FilteredTree_FilterMessage=type filter text
+FilteredTree_AccessibleListenerFiltered={0} {1} matches.

Back to the top