blob: 62edbd1138bd0dde843dcc47d7a32494600d71d5 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2015, 2017 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Sascha Becher - Bug 186404 - Update PatternFilter API to allow extensions
* Lucas Bullen (Red Hat Inc.) - [Bug 203792] filter should support multiple keywords
*******************************************************************************/
package org.eclipse.ui.dialogs;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.ContentViewer;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.ui.internal.misc.TextMatcher;
/**
* A filter used in conjunction with <code>FilteredTree</code>. In order to
* determine if a node should be filtered it uses the content and label provider
* of the tree to do pattern matching on its children. This causes the entire
* tree structure to be realized. Note that the label provider must implement
* ILabelProvider.
*
* @see org.eclipse.ui.dialogs.FilteredTree
* @since 3.2
*/
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 TextMatcher matcher;
private boolean useEarlyReturnIfMatcherIsNull = true;
private static Object[] EMPTY = new Object[0];
@Override
public 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 the pattern string.
*/
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.isEmpty()) {
matcher = null;
} else {
String pattern = patternString;
if (!patternString.endsWith(" ")) //$NON-NLS-1$
pattern += "*"; //$NON-NLS-1$
if (includeLeadingWildcard) {
pattern = "*" + pattern; //$NON-NLS-1$
}
matcher = new TextMatcher(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 the element to check
* @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) {
if (viewer instanceof AbstractTreeViewer
&& ((AbstractTreeViewer) viewer).getContentProvider() instanceof ITreeContentProvider) {
Object[] children = ((ITreeContentProvider) ((AbstractTreeViewer) viewer).getContentProvider())
.getChildren(element);
return children != null && children.length > 0 && 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) ((ContentViewer) viewer).getLabelProvider()).getText(element);
if (labelText == null) {
return false;
}
return wordMatches(labelText);
}
/**
* 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 = TextMatcher.getWords(text);
for (String word : words) {
if (!match(word)) {
return false;
}
}
return words.length > 0;
}
/**
* Can be called by the filtered tree to turn on caching.
*
* @param useCache The useCache to set.
*/
void setUseCache(boolean useCache) {
this.useCache = useCache;
}
}