/* ********************************************************************** ** ** Copyright notice ** ** ** ** (c) 2005-2009 RSSOwl Development Team ** ** http://www.rssowl.org/ ** ** ** ** 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.rssowl.org/legal/epl-v10.html ** ** ** ** A copy is found in the file epl-v10.html and important notices to the ** ** license from the team is found in the textfile LICENSE.txt distributed ** ** in this package. ** ** ** ** This copyright notice MUST APPEAR in all copies of the file! ** ** ** ** Contributors: ** ** RSSOwl Development Team - initial API and implementation ** ** ** ** ********************************************************************** */ package org.rssowl.ui.internal.views.explorer; import org.eclipse.jface.viewers.AbstractTreeViewer; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerFilter; import org.rssowl.core.persist.IBookMark; import org.rssowl.core.persist.IEntity; import org.rssowl.core.persist.IFolderChild; import org.rssowl.core.persist.INews; import org.rssowl.core.persist.INewsBin; import org.rssowl.core.persist.INewsMark; import org.rssowl.core.persist.ISearchMark; import org.rssowl.core.persist.event.ModelEvent; import org.rssowl.core.util.CoreUtils; import org.rssowl.core.util.StringMatcher; import java.text.BreakIterator; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.Set; /** * Filter matching the leaf-nodes of a Tree-Structure. Parent Nodes are made * visible as soon as leaf-nodes match the filter. This causes the entire tree * structure to be realized. * <p> * Note: Inline StringMatcher from 3.2 final as it is internal API. * </p> * * @author bpasero */ public class BookMarkFilter extends ViewerFilter { /** Possible Filter Values */ public enum Type { /** Show all Feeds */ SHOW_ALL, /** Show Feeds with new News */ SHOW_NEW, /** Show Feeds with unread News */ SHOW_UNREAD, /** Show Feeds that had an error while loading */ SHOW_ERRONEOUS, /** Show never visited Feeds */ SHOW_NEVER_VISITED, /** Show sticky Feeds */ SHOW_STICKY } /** Possible Search Targets */ public enum SearchTarget { /** Search Name */ NAME, /** Search Link */ LINK, } /* The string pattern matcher used for this pattern filter */ private StringMatcher fMatcher; /* Current Filter Value */ private Type fType = Type.SHOW_ALL; /* Current Search Target */ private SearchTarget fSearchTarget = SearchTarget.NAME; /* * @see org.eclipse.jface.viewers.ViewerFilter#select(org.eclipse.jface.viewers.Viewer, * java.lang.Object, java.lang.Object) */ @Override public final boolean select(Viewer viewer, Object parentElement, Object element) { /* Filter not Active */ if (fMatcher == null && fType == Type.SHOW_ALL) return true; return isElementVisible(viewer, element); } @Override public Object[] filter(Viewer viewer, Object parent, Object[] elements) { /* Filter not Active */ if (fMatcher == null && fType == BookMarkFilter.Type.SHOW_ALL) return elements; return super.filter(viewer, parent, elements); } @Override public boolean isFilterProperty(Object element, String property) { return false; // This is handled in needsRefresh() already } /** * Get the Target of the Search. The Target is describing which elements to * search when a Text-Search is performed. * * @return Returns the SearchTarget of the Search as described in the * <code>SearchTarget</code> enumeration. */ SearchTarget getSearchTarget() { return fSearchTarget; } /** * Set the Target of the Search. The Target is describing which elements to * search when a Text-Search is performed. * * @param searchTarget The SearchTarget of the Search as described in the * <code>SearchTarget</code> enumeration. */ public void setSearchTarget(SearchTarget searchTarget) { fSearchTarget = searchTarget; } /** * Set the Type of this Filter. The Type is describing which elements are * filtered. * * @param type The Type of this Filter as described in the <code>Type</code> * enumeration. */ public void setType(Type type) { if (fType != type) fType = type; } /** * Get the Type of this Filter. The Type is describing which elements are * filtered. * * @return Returns the Type of this Filter as described in the * <code>Type</code> enumeration. */ Type getType() { return fType; } /** * The pattern string for which this filter should select elements in the * viewer. * * @param patternString */ public void setPattern(String patternString) { if (patternString == null || patternString.equals("")) //$NON-NLS-1$ fMatcher = null; else fMatcher = new StringMatcher(patternString + "*", true, false); //$NON-NLS-1$ } /** * 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 (fMatcher == null) return true; return fMatcher.match(string); } boolean needsRefresh(Class<? extends IEntity> entityClass, Set<? extends ModelEvent> events) { return needsRefresh(entityClass, events, false); } boolean needsRefresh(Class<? extends IEntity> entityClass, Set<? extends ModelEvent> events, boolean searchResultsChanged) { /* In case the Filter is not active at all */ if (fMatcher == null && fType == Type.SHOW_ALL) return false; /* News Event */ if (entityClass.equals(INews.class)) { if (fType == Type.SHOW_NEW) return CoreUtils.isNewStateChange(events); else if (fType == Type.SHOW_UNREAD) return CoreUtils.isReadStateChange(events); else if (fType == Type.SHOW_STICKY) return CoreUtils.isStickyStateChange(events); } /* Bookmark Event */ else if (IBookMark.class.isAssignableFrom(entityClass)) { if (fMatcher != null) return true; if (fType == Type.SHOW_NEVER_VISITED || fType == Type.SHOW_ERRONEOUS) return true; } /* Searchmark / News Bin Event */ else if (ISearchMark.class.isAssignableFrom(entityClass) || INewsBin.class.isAssignableFrom(entityClass)) { if (fMatcher != null && !searchResultsChanged) return true; if (fType == Type.SHOW_NEW || fType == Type.SHOW_UNREAD) return true; } return false; } /** * 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 */ boolean isElementVisible(Viewer viewer, Object element) { return isParentMatch(viewer, element) || isLeafMatch(viewer, element); } /** * 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 */ boolean isElementSelectable(Object element) { return element != null; } /** * 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 */ private boolean isParentMatch(Viewer viewer, Object element) { if (!(viewer instanceof AbstractTreeViewer)) return false; Object[] children = ((ITreeContentProvider) ((AbstractTreeViewer) viewer).getContentProvider()).getChildren(element); if ((children != null) && (children.length > 0)) return filter(viewer, element, children).length > 0; 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) { /* Element is a News Mark */ if (element instanceof INewsMark) { INewsMark newsmark = (INewsMark) element; boolean isMatch = false; /* Look at Type */ switch (fType) { case SHOW_ALL: isMatch = true; break; /* Show: Feeds with New News */ case SHOW_NEW: isMatch = newsmark.getNewsCount(EnumSet.of(INews.State.NEW)) > 0; break; /* Show: Unread Marks */ case SHOW_UNREAD: isMatch = newsmark.getNewsCount(EnumSet.of(INews.State.NEW, INews.State.UNREAD, INews.State.UPDATED)) > 0; break; /* Show: Sticky Marks */ case SHOW_STICKY: if (newsmark instanceof IBookMark) { IBookMark bookmark = (IBookMark) newsmark; isMatch = bookmark.getStickyNewsCount() > 0; } break; /* Show: Feeds that had an Error while loading */ case SHOW_ERRONEOUS: if (newsmark instanceof IBookMark) isMatch = ((IBookMark) newsmark).isErrorLoading(); break; /* Show: Never visited Marks */ case SHOW_NEVER_VISITED: isMatch = newsmark.getPopularity() <= 0; break; } /* Finally check the Pattern */ if (isMatch && fMatcher != null) { if (!wordMatches(newsmark) && !wordMatches(newsmark.getParent())) return false; } return isMatch; } return false; } /** * 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 words.toArray(new String[words.size()]); } private boolean wordMatches(IFolderChild node) { /* Return early if node is a Bookmark-Set */ if (node.getParent() == null) return false; /* Search Name */ if (fSearchTarget == SearchTarget.NAME) return wordMatches(node.getName()); /* Search Link */ if (fSearchTarget == SearchTarget.LINK && node instanceof IBookMark) return wordMatches(((IBookMark) node).getFeedLinkReference().getLinkAsText()); return false; } /** * 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; } }