/*******************************************************************************
* Copyright (c) 2006 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* IBM Research
*******************************************************************************/
package net.sourceforge.tagsea.core.ui.internal.views;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.Widget;
/**
* Filters a tree based on words found in a pattern string. The words are
* seperated by whitespace, and then checked against the tree labels to
* see if any of the words match. If an item in the tree matches the pattern,
* then the path from the root to that item passes teh filter, as do all of
* the children of the matching element.
*
* Also implements IStringFinder so that the regions of the match can be found.
* @author Del Myers
*/
public class CachingTreePatternFilter extends AbstractPatternFilter implements IStringFinder
{
/**
* The string pattern matcher used for this pattern filter.
*/
private Vector<StringMatcher> matchers;
/**
* A map that caches visible elements for the specified viewer.
*/
private HashMap<Viewer, HashSet<Object>> visibleElementsMap;
/**
* A map of viewer inputs to determine if the input of a viewer
* has changed since the last invocation.
*/
private HashMap<Viewer, Object> viewerInputs;
/**
* A map for watching dispose events.
*/
private HashMap<Control, Viewer> disposeMap;
private ViewerDisposeListener viewerDisposeListener;
//private ViewerListener listener;
private class ViewerDisposeListener implements DisposeListener {
/* (non-Javadoc)
* @see org.eclipse.swt.events.DisposeListener#widgetDisposed(org.eclipse.swt.events.DisposeEvent)
*/
public void widgetDisposed(DisposeEvent e) {
unhookViewer(disposeMap.get((Control)e.widget));
}
}
public CachingTreePatternFilter() {
this.viewerDisposeListener = new ViewerDisposeListener();
this.visibleElementsMap = new HashMap<Viewer, HashSet<Object>>();
this.viewerInputs = new HashMap<Viewer, Object>();
this.disposeMap = new HashMap<Control, Viewer>();
}
/* (non-Javadoc)
* @see org.eclipse.jface.viewers.ViewerFilter#filter(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object[])
*/
public final Object[] filter(Viewer viewer, Object parent, Object[] elements)
{
if (matchers == null)
return elements;
return super.filter(viewer, parent, elements);
}
/* (non-Javadoc)
* @see org.eclipse.jface.viewers.ViewerFilter#select(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object)
*/
public final boolean select(Viewer viewer, Object parentElement, Object element)
{
return isElementVisible(viewer, element);
}
/**
* The pattern string for which this filter should select
* elements in the viewer.
*
* @param patternString
*/
public void setPattern(String patternString)
{
uhookAllViewers();
if (patternString == null || patternString.equals("")) { //$NON-NLS-1$
matchers = null;
}
else
{
// Split on whitespace
String[] words = patternString.split("[ \t]");
for(int i = 0 ; i < words.length; i++)
{
if(!words[i].endsWith("*"))
words[i] = words[i] + "*";
}
if(words.length > 0)
{
matchers = new Vector<StringMatcher>();
for(String word : words)
{
matchers.add(new StringMatcher(word, true, false));
}
}
}
}
/**
* Unhooks all the viewers to clear the cache.
*/
private void uhookAllViewers() {
ArrayList<Viewer> viewers = new ArrayList<Viewer>(visibleElementsMap.keySet());
for (Viewer viewer : viewers) {
unhookViewer(viewer);
}
}
/**
* 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 (matchers == null)
{
return true;
}
for(StringMatcher matcher : matchers)
{
if(matcher.match(string))
return true;
}
return false;
}
/**
* 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){
if (!visibleElementsMap.containsKey(viewer)) {
hookViewer(viewer);
} else if (viewer.getInput() != viewerInputs.get(viewer)) {
hookViewer(viewer);
}
HashSet<Object> visibleElements = visibleElementsMap.get(viewer);
return visibleElements.contains(element);
}
/**
* Hooks the given viewer and computes all of the visible elements
* in it, and caches them in order that the tree need only be
* walked once.
* @param viewer
*/
private void hookViewer(Viewer viewer) {
unhookViewer(viewer);
viewer.getControl().addDisposeListener(viewerDisposeListener);
viewerInputs.put(viewer, viewer.getInput());
visibleElementsMap.put(viewer, new HashSet<Object>());
calculateVisibility(viewer);
}
/**
* @param viewer
*/
private void calculateVisibility(Viewer viewer) {
if (!(viewer instanceof TreeViewer)) return;
ITreeContentProvider provider =
(ITreeContentProvider) ((TreeViewer)viewer).getContentProvider();
Object[] elements = provider.getElements(viewer.getInput());
calculateChildVisibility(viewer, viewer.getInput(), elements, provider);
}
/**
* @param elements
* @param provider
* @return true if the caller should be made visible due to one or more if its children being visible.
*/
private boolean calculateChildVisibility(Viewer viewer, Object parent, Object[] children, ITreeContentProvider provider) {
boolean visible = false;
for (int i = 0; i < children.length; i++) {
Object child = children[i];
if (isLeafMatch(viewer, child)) {
visible = true;
addAllChildren(viewer, child, provider);
} else {
if (provider.hasChildren(child)) {
visible |= calculateChildVisibility(viewer, child, provider.getChildren(child), provider);
}
}
}
if (visible) {
visibleElementsMap.get(viewer).add(parent);
}
return visible;
}
/**
* @param viewer
* @param child
* @param children
* @param provider
*/
private void addAllChildren(Viewer viewer, Object parent, ITreeContentProvider provider) {
visibleElementsMap.get(viewer).add(parent);
Object[] children = new Object[0];
if (provider.hasChildren(parent)) {
children = provider.getChildren(parent);
}
for (int i = 0; i < children.length; i++) {
Object child = children[i];
visibleElementsMap.get(viewer).add(child);
if (provider.hasChildren(child)) {
addAllChildren(viewer, child, provider);
}
}
}
private void unhookViewer(Viewer viewer) {
viewerInputs.remove(viewer);
visibleElementsMap.remove(viewer);
if (!viewer.getControl().isDisposed()) {
viewer.getControl().removeDisposeListener(viewerDisposeListener);
}
disposeMap.remove(viewer.getControl());
}
/**
* 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
*/
public boolean isLeafMatch(Viewer viewer, Object element)
{
if (element == viewer.getInput()) return false;
if (!(viewer instanceof StructuredViewer)) return false;
StructuredViewer sv = (StructuredViewer) viewer;
String labelText = null;
Widget widget = sv.testFindItem(element);
if (widget instanceof Item) {
labelText = ((Item)widget).getText();
}
if (labelText == null) {
if (!(sv.getLabelProvider() instanceof ILabelProvider)) return false;
ILabelProvider labelProvider = (ILabelProvider) sv.getLabelProvider();
labelText = labelProvider.getText(element);
}
if(labelText == null) {
return false;
}
//check all words.
String[] fullText = labelText.split("\\.");
boolean match = false;
for (int i = 0; i < fullText.length; i++) {
match = wordMatches(fullText[i]);
if (match) return match;
}
return match;
}
/**
* 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<String> words = new ArrayList<String>();
// 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 (int i = 0; i < words.length; i++)
{
String word = words[i];
if (match(word))
{
return true;
}
}
return false;
}
/* (non-Javadoc)
* @see net.sourceforge.tagsea.core.ui.internal.views.IStringFinder#findRegions(java.lang.String)
*/
public IRegion[] findRegions(String text) {
//check all words.
String[] fullText = text.split("\\.");
boolean match = false;
for (int i = 0; i < fullText.length; i++) {
match = wordMatches(fullText[i]);
if (match) break;
}
if (!match) return new IRegion[0];
LinkedList<IRegion> regions = new LinkedList<IRegion>();
if (matchers == null) return new IRegion[0];
for (StringMatcher matcher : matchers) {
StringMatcher.Position p = matcher.find(text, 0, text.length());
if (p != null && !(p.start==0 && p.end==0)) {
int offset = p.getStart();
int length = p.getEnd()-p.getStart();
regions.add(new Region(offset, length));
}
}
return sortAndOrganize(regions);
}
/**
* @param regions
* @return
*/
private IRegion[] sortAndOrganize(LinkedList<IRegion> regions) {
Collections.sort(regions, new Comparator<IRegion>() {
public int compare(IRegion o1, IRegion o2) {
return o1.getOffset()-o2.getOffset();
}
});
for (int i = 0; i < regions.size(); i++) {
//check for overlap
if (i > 0) {
IRegion last = regions.get(i-1);
IRegion next = regions.get(i);
int end = last.getOffset() + last.getLength();
if ((next.getOffset() <= end)) {
int offset = last.getOffset();
int newEnd = Math.max(end, next.getOffset()+next.getLength());
IRegion newRegion = new Region(offset, newEnd-offset);
regions.add(i-1, newRegion);
regions.remove(i);
regions.remove(i);
}
}
}
return regions.toArray(new IRegion[regions.size()]);
}
}