diff options
| author | Simon Scholz | 2014-10-30 19:35:51 +0000 |
|---|---|---|
| committer | Lars Vogel | 2014-11-03 06:59:29 +0000 |
| commit | 4b92a487836c4e58073e0fad0650d69b7e0c45ea (patch) | |
| tree | 5b8ff416bdfd3a5d454a6d8e1c854b1134a9f5af | |
| parent | d04a5dfd9d252e9202b435f22b5f69d805358150 (diff) | |
| download | eclipse.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>
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 Binary files differnew file mode 100644 index 00000000000..39ed58beb7d --- /dev/null +++ b/bundles/org.eclipse.e4.ui.dialogs/icons/full/dtool16/clear_co.png 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 Binary files differnew file mode 100644 index 00000000000..b09a154a2db --- /dev/null +++ b/bundles/org.eclipse.e4.ui.dialogs/icons/full/etool16/clear_co.png 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. |
