001: /*******************************************************************************
002: * Copyright (c) 2004, 2007 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.ui.dialogs;
011:
012: import java.util.ArrayList;
013: import java.util.HashMap;
014: import java.util.List;
015: import java.util.Map;
016:
017: import org.eclipse.jface.viewers.AbstractTreeViewer;
018: import org.eclipse.jface.viewers.ILabelProvider;
019: import org.eclipse.jface.viewers.ITreeContentProvider;
020: import org.eclipse.jface.viewers.StructuredViewer;
021: import org.eclipse.jface.viewers.Viewer;
022: import org.eclipse.jface.viewers.ViewerFilter;
023: import org.eclipse.ui.internal.misc.StringMatcher;
024:
025: import com.ibm.icu.text.BreakIterator;
026:
027: /**
028: * A filter used in conjunction with <code>FilteredTree</code>. In order to
029: * determine if a node should be filtered it uses the content provider of the
030: * tree to do pattern matching on its children. This causes the entire tree
031: * structure to be realized.
032: *
033: * @see org.eclipse.ui.dialogs.FilteredTree
034: * @since 3.2
035: */
036: public class PatternFilter extends ViewerFilter {
037: /*
038: * Cache of filtered elements in the tree
039: */
040: private Map cache = new HashMap();
041:
042: /*
043: * Maps parent elements to TRUE or FALSE
044: */
045: private Map foundAnyCache = new HashMap();
046:
047: private boolean useCache = false;
048:
049: /**
050: * Whether to include a leading wildcard for all provided patterns. A
051: * trailing wildcard is always included.
052: */
053: private boolean includeLeadingWildcard = false;
054:
055: /**
056: * The string pattern matcher used for this pattern filter.
057: */
058: private StringMatcher matcher;
059:
060: private boolean useEarlyReturnIfMatcherIsNull = true;
061:
062: private static Object[] EMPTY = new Object[0];
063:
064: /* (non-Javadoc)
065: * @see org.eclipse.jface.viewers.ViewerFilter#filter(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object[])
066: */
067: public final Object[] filter(Viewer viewer, Object parent,
068: Object[] elements) {
069: // we don't want to optimize if we've extended the filter ... this
070: // needs to be addressed in 3.4
071: // https://bugs.eclipse.org/bugs/show_bug.cgi?id=186404
072: if (matcher == null && useEarlyReturnIfMatcherIsNull) {
073: return elements;
074: }
075:
076: if (!useCache) {
077: return super .filter(viewer, parent, elements);
078: }
079:
080: Object[] filtered = (Object[]) cache.get(parent);
081: if (filtered == null) {
082: Boolean foundAny = (Boolean) foundAnyCache.get(parent);
083: if (foundAny != null && !foundAny.booleanValue()) {
084: filtered = EMPTY;
085: } else {
086: filtered = super .filter(viewer, parent, elements);
087: }
088: cache.put(parent, filtered);
089: }
090: return filtered;
091: }
092:
093: /**
094: * Returns true if any of the elements makes it through the filter.
095: * This method uses caching if enabled; the computation is done in
096: * computeAnyVisible.
097: *
098: * @param viewer
099: * @param parent
100: * @param elements the elements (must not be an empty array)
101: * @return true if any of the elements makes it through the filter.
102: */
103: private boolean isAnyVisible(Viewer viewer, Object parent,
104: Object[] elements) {
105: if (matcher == null) {
106: return true;
107: }
108:
109: if (!useCache) {
110: return computeAnyVisible(viewer, elements);
111: }
112:
113: Object[] filtered = (Object[]) cache.get(parent);
114: if (filtered != null) {
115: return filtered.length > 0;
116: }
117: Boolean foundAny = (Boolean) foundAnyCache.get(parent);
118: if (foundAny == null) {
119: foundAny = computeAnyVisible(viewer, elements) ? Boolean.TRUE
120: : Boolean.FALSE;
121: foundAnyCache.put(parent, foundAny);
122: }
123: return foundAny.booleanValue();
124: }
125:
126: /**
127: * Returns true if any of the elements makes it through the filter.
128: * @param viewer
129: * @param elements
130: * @return
131: */
132: private boolean computeAnyVisible(Viewer viewer, Object[] elements) {
133: boolean elementFound = false;
134: for (int i = 0; i < elements.length && !elementFound; i++) {
135: Object element = elements[i];
136: elementFound = isElementVisible(viewer, element);
137: }
138: return elementFound;
139: }
140:
141: /* (non-Javadoc)
142: * @see org.eclipse.jface.viewers.ViewerFilter#select(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object)
143: */
144: public final boolean select(Viewer viewer, Object parentElement,
145: Object element) {
146: return isElementVisible(viewer, element);
147: }
148:
149: /**
150: * Sets whether a leading wildcard should be attached to each pattern
151: * string.
152: *
153: * @param includeLeadingWildcard
154: * Whether a leading wildcard should be added.
155: */
156: public final void setIncludeLeadingWildcard(
157: final boolean includeLeadingWildcard) {
158: this .includeLeadingWildcard = includeLeadingWildcard;
159: }
160:
161: /**
162: * The pattern string for which this filter should select
163: * elements in the viewer.
164: *
165: * @param patternString
166: */
167: public void setPattern(String patternString) {
168: // these 2 strings allow the PatternFilter to be extended in
169: // 3.3 - https://bugs.eclipse.org/bugs/show_bug.cgi?id=186404
170: if ("org.eclipse.ui.keys.optimization.true".equals(patternString)) { //$NON-NLS-1$
171: useEarlyReturnIfMatcherIsNull = true;
172: return;
173: } else if ("org.eclipse.ui.keys.optimization.false".equals(patternString)) { //$NON-NLS-1$
174: useEarlyReturnIfMatcherIsNull = false;
175: return;
176: }
177: clearCaches();
178: if (patternString == null || patternString.equals("")) { //$NON-NLS-1$
179: matcher = null;
180: } else {
181: String pattern = patternString + "*"; //$NON-NLS-1$
182: if (includeLeadingWildcard) {
183: pattern = "*" + pattern; //$NON-NLS-1$
184: }
185: matcher = new StringMatcher(pattern, true, false);
186: }
187: }
188:
189: /**
190: * Clears the caches used for optimizing this filter. Needs to be called whenever
191: * the tree content changes.
192: */
193: /* package */void clearCaches() {
194: cache.clear();
195: foundAnyCache.clear();
196: }
197:
198: /**
199: * Answers whether the given String matches the pattern.
200: *
201: * @param string the String to test
202: *
203: * @return whether the string matches the pattern
204: */
205: private boolean match(String string) {
206: if (matcher == null) {
207: return true;
208: }
209: return matcher.match(string);
210: }
211:
212: /**
213: * Answers whether the given element is a valid selection in
214: * the filtered tree. For example, if a tree has items that
215: * are categorized, the category itself may not be a valid
216: * selection since it is used merely to organize the elements.
217: *
218: * @param element
219: * @return true if this element is eligible for automatic selection
220: */
221: public boolean isElementSelectable(Object element) {
222: return element != null;
223: }
224:
225: /**
226: * Answers whether the given element in the given viewer matches
227: * the filter pattern. This is a default implementation that will
228: * show a leaf element in the tree based on whether the provided
229: * filter text matches the text of the given element's text, or that
230: * of it's children (if the element has any).
231: *
232: * Subclasses may override this method.
233: *
234: * @param viewer the tree viewer in which the element resides
235: * @param element the element in the tree to check for a match
236: *
237: * @return true if the element matches the filter pattern
238: */
239: public boolean isElementVisible(Viewer viewer, Object element) {
240: return isParentMatch(viewer, element)
241: || isLeafMatch(viewer, element);
242: }
243:
244: /**
245: * Check if the parent (category) is a match to the filter text. The default
246: * behavior returns true if the element has at least one child element that is
247: * a match with the filter text.
248: *
249: * Subclasses may override this method.
250: *
251: * @param viewer the viewer that contains the element
252: * @param element the tree element to check
253: * @return true if the given element has children that matches the filter text
254: */
255: protected boolean isParentMatch(Viewer viewer, Object element) {
256: Object[] children = ((ITreeContentProvider) ((AbstractTreeViewer) viewer)
257: .getContentProvider()).getChildren(element);
258:
259: if ((children != null) && (children.length > 0)) {
260: return isAnyVisible(viewer, element, children);
261: }
262: return false;
263: }
264:
265: /**
266: * Check if the current (leaf) element is a match with the filter text.
267: * The default behavior checks that the label of the element is a match.
268: *
269: * Subclasses should override this method.
270: *
271: * @param viewer the viewer that contains the element
272: * @param element the tree element to check
273: * @return true if the given element's label matches the filter text
274: */
275: protected boolean isLeafMatch(Viewer viewer, Object element) {
276: String labelText = ((ILabelProvider) ((StructuredViewer) viewer)
277: .getLabelProvider()).getText(element);
278:
279: if (labelText == null) {
280: return false;
281: }
282: return wordMatches(labelText);
283: }
284:
285: /**
286: * Take the given filter text and break it down into words using a
287: * BreakIterator.
288: *
289: * @param text
290: * @return an array of words
291: */
292: private String[] getWords(String text) {
293: List words = new ArrayList();
294: // Break the text up into words, separating based on whitespace and
295: // common punctuation.
296: // Previously used String.split(..., "\\W"), where "\W" is a regular
297: // expression (see the Javadoc for class Pattern).
298: // Need to avoid both String.split and regular expressions, in order to
299: // compile against JCL Foundation (bug 80053).
300: // Also need to do this in an NL-sensitive way. The use of BreakIterator
301: // was suggested in bug 90579.
302: BreakIterator iter = BreakIterator.getWordInstance();
303: iter.setText(text);
304: int i = iter.first();
305: while (i != java.text.BreakIterator.DONE && i < text.length()) {
306: int j = iter.following(i);
307: if (j == java.text.BreakIterator.DONE) {
308: j = text.length();
309: }
310: // match the word
311: if (Character.isLetterOrDigit(text.charAt(i))) {
312: String word = text.substring(i, j);
313: words.add(word);
314: }
315: i = j;
316: }
317: return (String[]) words.toArray(new String[words.size()]);
318: }
319:
320: /**
321: * Return whether or not if any of the words in text satisfy the
322: * match critera.
323: *
324: * @param text the text to match
325: * @return boolean <code>true</code> if one of the words in text
326: * satisifes the match criteria.
327: */
328: protected boolean wordMatches(String text) {
329: if (text == null) {
330: return false;
331: }
332:
333: //If the whole text matches we are all set
334: if (match(text)) {
335: return true;
336: }
337:
338: // Otherwise check if any of the words of the text matches
339: String[] words = getWords(text);
340: for (int i = 0; i < words.length; i++) {
341: String word = words[i];
342: if (match(word)) {
343: return true;
344: }
345: }
346:
347: return false;
348: }
349:
350: /**
351: * Can be called by the filtered tree to turn on caching.
352: *
353: * @param useCache The useCache to set.
354: */
355: void setUseCache(boolean useCache) {
356: this.useCache = useCache;
357: }
358: }
|