// BlogBridge -- RSS feed reader, manager, and web based service // Copyright (C) 2002-2006 by R. Pito Salas // // This program is free software; you can redistribute it and/or modify it under // the terms of the GNU General Public License as published by the Free Software Foundation; // either version 2 of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with this program; // if not, write to the Free Software Foundation, Inc., 59 Temple Place, // Suite 330, Boston, MA 02111-1307 USA // // Contact: R. Pito Salas // mailto:pitosalas@users.sourceforge.net // More information: about BlogBridge // http://www.blogbridge.com // http://sourceforge.net/projects/blogbridge // // $Id: NavigatorAdv.java,v 1.14 2008/03/17 12:23:06 spyromus Exp $ // package com.salas.bb.core; import com.salas.bb.domain.GuidesSet; import com.salas.bb.domain.IArticle; import com.salas.bb.domain.IFeed; import com.salas.bb.domain.IGuide; import com.salas.bb.utils.i18n.Strings; import com.salas.bb.views.feeds.IFeedDisplayConstants; import java.text.MessageFormat; import java.util.logging.Level; import java.util.logging.Logger; /** * This class is intended to provide navigation services. It drives the process of * switching between channels and guides on user's demand. It finds next or previous * targets for switching and makes this switching using <code>GlobalController</code> * services. */ public class NavigatorAdv extends ControllerAdapter { private static final Logger LOG = Logger.getLogger(NavigatorAdv.class.getName()); private GuideModel navigationModel; private GuideModel viewModel; private final GuidesListModel guidesListModel; private GuidesSet guidesSet; private IGuide selectedGuide; private IFeed selectedFeed; /** * Creates navigator for the model. * * @param aNavigationModel navigation model used for view. * @param guidesListModel guides list model. */ public NavigatorAdv(GuideModel aNavigationModel, GuidesListModel guidesListModel) { navigationModel = aNavigationModel; this.guidesListModel = guidesListModel; viewModel = null; } /** * Sets the view model that will be used for navavigation through visible guide. If this * model isn't set then the regular model will be used. * * @param aViewModel view model. */ public void setViewModel(GuideModel aViewModel) { viewModel = aViewModel; } /** * Sets the set of gudies to refer to when looking for next/previous guide. * * @param set guide set. */ public void setGuidesSet(GuidesSet set) { guidesSet = set; } /** * Invoked after application changes the guide. * * @param guide guide to with we have switched. */ public synchronized void guideSelected(IGuide guide) { selectedGuide = guide; selectedFeed = null; } /** * Invoked after application changes the channel. * * @param feed channel to which we are switching. */ public synchronized void feedSelected(IFeed feed) { selectedFeed = feed; } /** * Returns destination feed for the given action key. * * @param key key. * * @return feed or NULL if there's no destination. */ public synchronized Destination getDestination(NavigationInfoKey key) { Destination dest; if (key instanceof NavigationInfoKey.Next) { boolean unreadOnly = key == NavigationInfoKey.NEXT_UNREAD; dest = recalcNext(unreadOnly); } else { boolean unreadOnly = key == NavigationInfoKey.PREV_UNREAD; dest = recalcPrev(unreadOnly); } return dest; } /** * Called by <code>Calculator</code> in separate thread. * * @param unreadOnly if TRUE feeds with unread articles only will be returned. * * @return destination for "Next" command. */ private synchronized Destination recalcNext(boolean unreadOnly) { IFeed destFeed = null; IFeed feed; IGuide guide; GuideModel model; feed = selectedFeed; guide = selectedGuide; if (guide == null) return null; // We are ready to start int pass = 0; while (destFeed == null && pass < 2) { // Get model for the guide if (guide != null && isVisible(guide)) { model = getModel(guide); feed = getNextInGuide(model, feed, unreadOnly); } // If feed isn't empty then we probably found what we needed. if (feed != null) { destFeed = feed; } else { // We increment pass counter each time the guide is what we started with. // On the leaving the guide for the second time we will break the loop. if (guide == selectedGuide) pass++; guide = getNextGuide(guide); } } return destFeed == null ? null : new Destination(guide, destFeed); } /** * Called by <code>Calculator</code> in separate thread. * * @param unreadOnly if TRUE feeds with unread articles only will be returned. * * @return destination for "Previous" command. */ private synchronized Destination recalcPrev(boolean unreadOnly) { IFeed destFeed = null; IFeed feed; IGuide guide; GuideModel model; feed = selectedFeed; guide = selectedGuide; if (guide == null) return null; // We are ready to start int pass = 0; while (destFeed == null && pass < 2) { // Get model for the guide if (guide != null && isVisible(guide)) { model = getModel(guide); feed = getPrevInGuide(model, feed, unreadOnly); } // If feed isn't empty then we probably found what we needed. if (feed != null) { destFeed = feed; } else { // We increment pass counter each time the guide is what we started with. // On the leaving the guide for the second time we will break the loop. if (guide == selectedGuide) pass++; guide = getPrevGuide(guide); } } return destFeed == null ? null : new Destination(guide, destFeed); } private boolean isVisible(IGuide guide) { return guidesListModel == null || guidesListModel.indexOf(guide) != -1; } /** * Returns mode initialized for the guide. * * @param guide guide. * * @return model. */ private GuideModel getModel(IGuide guide) { GuideModel model; if (guide == selectedGuide && viewModel != null) { model = viewModel; } else { model = navigationModel; navigationModel.setGuide(guide); } return model; } /** * Looks for the feed, which is next in relation to <code>currentFeed</code> in the given * <code>model</code>. If <code>unreadOnly</code> or <code>keywordsOnly</code> properties set * then only appropriate feeds will be choosen. * * @param model model to analyze. * @param currentFeed current feed to start searching from or NULL if to start from the start. * @param unreadOnly if TRUE feeds with unread articles only will be returned. * * @return next feed, <code>stopFeed</code> or NULL (if next feed not found). */ IFeed getNextInGuide(GuideModel model, IFeed currentFeed, boolean unreadOnly) { int size = model.getSize(); int index = currentFeed == null ? -1 : model.indexOf(currentFeed); IFeed next = null; if (currentFeed != null && index == -1) { LOG.log(Level.SEVERE, MessageFormat.format(Strings.error("feed.does.not.belong.to.model.feed"), currentFeed)); } else { for (int i = index + 1; next == null && i < size; i++) { next = (IFeed)model.getElementAt(i); // Make sure feed matches expectations according to the unreadOnly parameter if (!isFeedMatching(next, unreadOnly, getArticleFilter())) next = null; } } return next; } /** * Looks for the feed, which is previous in relation to <code>currentFeed</code> in the given * <code>model</code>. If <code>unreadOnly</code> or <code>keywordsOnly</code> properties set * then only appropriate feeds will be choosen. * * @param model model to analyze. * @param currentFeed current feed to start searching from or NULL if to start from the start. * @param unreadOnly if TRUE feeds with unread articles only will be returned. * * @return previous feed, <code>stopFeed</code> or NULL (if next feed not found). */ IFeed getPrevInGuide(GuideModel model, IFeed currentFeed, boolean unreadOnly) { int size = model.getSize(); int index = currentFeed == null ? size : model.indexOf(currentFeed); IFeed prev = null; if (currentFeed != null && index == -1) { LOG.log(Level.SEVERE, MessageFormat.format(Strings.error("feed.does.not.belong.to.model.feed"), currentFeed)); } else { for (int i = index - 1; prev == null && i >= 0; i--) { prev = (IFeed)model.getElementAt(i); // Make sure feed matches expectations according to the unreadOnly parameter if (!isFeedMatching(prev, unreadOnly, getArticleFilter())) prev = null; } } return prev; } /** * Returns TRUE if the feed matches the selection criteria. * * @param feed feed. * @param unreadOnly unread-only flag. * @param filter article filtering mode (IFeedDisplayConstants.FILTER_XYZ). * * @return TRUE if the feed matches the selection criteria. */ static boolean isFeedMatching(IFeed feed, boolean unreadOnly, int filter) { boolean hasVisibleArticles = false; int count = feed.getArticlesCount(); if (count > 0) { if (filter == IFeedDisplayConstants.FILTER_UNREAD) { // If showing unread, this will be sufficient hasVisibleArticles = feed.getUnreadArticlesCount() > 0; } else if (filter == IFeedDisplayConstants.FILTER_ALL) { // If showing all, this will be sufficient hasVisibleArticles = !unreadOnly || feed.getUnreadArticlesCount() > 0; } else { // For all other filters, iterate until all articles are scanned, or // the first visible is found. for (int i = 0; !hasVisibleArticles && i < count; i++) { IArticle article = feed.getArticleAt(i); if (!unreadOnly || !article.isRead()) { // If read state match, see others switch (filter) { case IFeedDisplayConstants.FILTER_PINNED: hasVisibleArticles = article.isPinned(); break; case IFeedDisplayConstants.FILTER_NEGATIVE: hasVisibleArticles = article.isNegative(); break; case IFeedDisplayConstants.FILTER_POSITIVE: hasVisibleArticles = article.isPositive(); break; case IFeedDisplayConstants.FILTER_NON_NEGATIVE: hasVisibleArticles = !article.isNegative(); break; } } } } } return hasVisibleArticles; } /** * Returns guide which is next to the current in the channel guides set. * * @param currentGuide current guide. * * @return next guide. */ IGuide getNextGuide(IGuide currentGuide) { IGuide next = currentGuide; if (guidesSet == null) { LOG.warning(Strings.error("guide.set.not.registered")); } else { next = nextGuide(currentGuide); } return next; } private IGuide nextGuide(IGuide aCurrentGuide) { int index = aCurrentGuide == null ? -1 : guidesSet.indexOf(aCurrentGuide); int next = index + 1; int size = guidesSet.getGuidesCount(); return guidesSet.getGuideAt(next >= size ? 0 : next); } /** * Returns guide which is previous to the current in the channel guides set. * * @param currentGuide current guide. * * @return previous guide. */ IGuide getPrevGuide(IGuide currentGuide) { IGuide prev = currentGuide; if (guidesSet == null) { LOG.warning(Strings.error("guide.set.not.registered")); } else { prev = prevChannelGuide(currentGuide); } return prev; } private IGuide prevChannelGuide(IGuide aCurrentGuide) { int size = guidesSet.getGuidesCount(); int index = aCurrentGuide == null ? size : guidesSet.indexOf(aCurrentGuide); int prev = index - 1; return guidesSet.getGuideAt(prev < 0 ? size - 1 : prev); } /** * Returns current article filter. * * @return filter. */ private static int getArticleFilter() { return GlobalModel.SINGLETON.getGlobalRenderingSettings().getArticleFilter(); } // ------------------------------------------------------------------------ // Supplementary classes // ------------------------------------------------------------------------ /** * Navigation destination. */ public static class Destination { public IGuide guide; public IFeed feed; /** * Creates holder. * * @param aGuide guide. * @param aFeed feed. */ public Destination(IGuide aGuide, IFeed aFeed) { guide = aGuide; feed = aFeed; } } /** * Keys for navigation info recalculation. */ public static interface NavigationInfoKey { /** * Next. */ NavigationInfoKey NEXT = new Next(); /** * Next (unread only). */ NavigationInfoKey NEXT_UNREAD = new Next(); /** * Previous. */ NavigationInfoKey PREV = new Prev(); /** * Previous (unread only). */ NavigationInfoKey PREV_UNREAD = new Prev(); /** * Marker class for next-operations. */ static final class Next implements NavigationInfoKey { } /** * Marker class for previous-operations. */ static final class Prev implements NavigationInfoKey { } } }