Package org.rssowl.ui.internal.editors.feed

Source Code of org.rssowl.ui.internal.editors.feed.NewsBrowserViewer

/*   **********************************************************************  **
**   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.editors.feed;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.GroupMarker;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ContentViewer;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.IContentProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.jface.window.SameShellProvider;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.IWorkbenchWindowActionDelegate;
import org.rssowl.core.Owl;
import org.rssowl.core.connection.ConnectionException;
import org.rssowl.core.connection.IProtocolHandler;
import org.rssowl.core.internal.persist.pref.DefaultPreferences;
import org.rssowl.core.persist.ICategory;
import org.rssowl.core.persist.IEntity;
import org.rssowl.core.persist.ILabel;
import org.rssowl.core.persist.IModelFactory;
import org.rssowl.core.persist.INews;
import org.rssowl.core.persist.INewsBin;
import org.rssowl.core.persist.INewsMark;
import org.rssowl.core.persist.IPerson;
import org.rssowl.core.persist.ISearch;
import org.rssowl.core.persist.ISearchCondition;
import org.rssowl.core.persist.ISearchField;
import org.rssowl.core.persist.SearchSpecifier;
import org.rssowl.core.persist.dao.DynamicDAO;
import org.rssowl.core.persist.dao.INewsDAO;
import org.rssowl.core.persist.event.NewsEvent;
import org.rssowl.core.persist.pref.IPreferenceScope;
import org.rssowl.core.persist.reference.NewsReference;
import org.rssowl.core.util.CoreUtils;
import org.rssowl.core.util.ITask;
import org.rssowl.core.util.ITask.Priority;
import org.rssowl.core.util.Pair;
import org.rssowl.core.util.StringUtils;
import org.rssowl.core.util.URIUtils;
import org.rssowl.ui.internal.Activator;
import org.rssowl.ui.internal.ApplicationActionBarAdvisor;
import org.rssowl.ui.internal.ApplicationServer;
import org.rssowl.ui.internal.Controller;
import org.rssowl.ui.internal.EntityGroup;
import org.rssowl.ui.internal.EntityGroupItem;
import org.rssowl.ui.internal.ILinkHandler;
import org.rssowl.ui.internal.OwlUI;
import org.rssowl.ui.internal.OwlUI.Layout;
import org.rssowl.ui.internal.OwlUI.PageSize;
import org.rssowl.ui.internal.actions.ArchiveNewsAction;
import org.rssowl.ui.internal.actions.AutomateFilterAction;
import org.rssowl.ui.internal.actions.CreateFilterAction.PresetAction;
import org.rssowl.ui.internal.actions.MakeNewsStickyAction;
import org.rssowl.ui.internal.actions.MarkAllNewsReadAction;
import org.rssowl.ui.internal.actions.MoveCopyNewsToBinAction;
import org.rssowl.ui.internal.actions.NavigationActionFactory;
import org.rssowl.ui.internal.actions.OpenInExternalBrowserAction;
import org.rssowl.ui.internal.actions.OpenNewsAction;
import org.rssowl.ui.internal.actions.ToggleReadStateAction;
import org.rssowl.ui.internal.dialogs.SearchNewsDialog;
import org.rssowl.ui.internal.editors.feed.NewsBrowserLabelProvider.Dynamic;
import org.rssowl.ui.internal.undo.NewsStateOperation;
import org.rssowl.ui.internal.undo.StickyOperation;
import org.rssowl.ui.internal.undo.UndoStack;
import org.rssowl.ui.internal.util.CBrowser;
import org.rssowl.ui.internal.util.JobRunner;
import org.rssowl.ui.internal.util.JobTracker;
import org.rssowl.ui.internal.util.ModelUtils;
import org.rssowl.ui.internal.util.UIBackgroundJob;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;

/**
* @author bpasero
*/
public class NewsBrowserViewer extends ContentViewer implements ILinkHandler {

  /* ID for Link Handlers */
  static final String MARK_READ_HANDLER_ID = "org.rssowl.ui.MarkRead"; //$NON-NLS-1$
  static final String TOGGLE_READ_HANDLER_ID = "org.rssowl.ui.ToggleRead"; //$NON-NLS-1$
  static final String TOGGLE_STICKY_HANDLER_ID = "org.rssowl.ui.ToggleSticky"; //$NON-NLS-1$
  static final String ARCHIVE_HANDLER_ID = "org.rssowl.ui.Archive"; //$NON-NLS-1$
  static final String DELETE_HANDLER_ID = "org.rssowl.ui.Delete"; //$NON-NLS-1$
  static final String ATTACHMENT_HANDLER_ID = "org.rssowl.ui.DownloadAttachment"; //$NON-NLS-1$
  static final String ATTACHMENTS_MENU_HANDLER_ID = "org.rssowl.ui.AttachmentsMenu"; //$NON-NLS-1$
  static final String LABELS_MENU_HANDLER_ID = "org.rssowl.ui.LabelsMenu"; //$NON-NLS-1$
  static final String EXPAND_NEWS_HANDLER_ID = "org.rssowl.ui.ExpandNews"; //$NON-NLS-1$
  static final String COLLAPSE_NEWS_HANDLER_ID = "org.rssowl.ui.CollapseNews"; //$NON-NLS-1$
  static final String EXPAND_GROUP_HANDLER_ID = "org.rssowl.ui.ExpandGroup"; //$NON-NLS-1$
  static final String COLLAPSE_GROUP_HANDLER_ID = "org.rssowl.ui.CollapseGroup"; //$NON-NLS-1$
  static final String GROUP_MENU_HANDLER_ID = "org.rssowl.ui.GroupMenu"; //$NON-NLS-1$
  static final String NEWS_MENU_HANDLER_ID = "org.rssowl.ui.NewsMenu"; //$NON-NLS-1$
  static final String SHARE_NEWS_MENU_HANDLER_ID = "org.rssowl.ui.ShareNewsMenu"; //$NON-NLS-1$
  static final String NEXT_NEWS_HANDLER_ID = "org.rssowl.ui.NextNews"; //$NON-NLS-1$
  static final String NEXT_UNREAD_NEWS_HANDLER_ID = "org.rssowl.ui.NextUnreadNews"; //$NON-NLS-1$
  static final String PREVIOUS_NEWS_HANDLER_ID = "org.rssowl.ui.PreviousNews"; //$NON-NLS-1$
  static final String PREVIOUS_UNREAD_NEWS_HANDLER_ID = "org.rssowl.ui.PreviousUnreadNews"; //$NON-NLS-1$
  static final String TRANSFORM_HANDLER_ID = "org.rssowl.ui.TransformNews"; //$NON-NLS-1$
  static final String RELATED_NEWS_MENU_HANDLER_ID = "org.rssowl.ui.RelatedNewsMenu"; //$NON-NLS-1$
  static final String NEXT_PAGE_HANDLER_ID = "org.rssowl.ui.NextPage"; //$NON-NLS-1$
  static final String SCROLL_NEXT_PAGE_HANDLER_ID = "org.rssowl.ui.ScrollNextPage"; //$NON-NLS-1$

  /* Delay in millies before reacting on user interaction */
  private static final int USER_INTERACTION_DELAY = 500;

  /* Unique identifier of the <body> element */
  private static final String BODY_ELEMENT_ID = "owlbody"; //$NON-NLS-1$

  private Object fInput;
  private CBrowser fBrowser;
  private IFeedViewSite fSite;
  private boolean fIsEmbedded;
  private Menu fNewsContextMenu;
  private Menu fAttachmentsContextMenu;
  private Menu fLabelsContextMenu;
  private Menu fShareNewsContextMenu;
  private Menu fFindRelatedContextMenu;
  private IStructuredSelection fCurrentSelection = StructuredSelection.EMPTY;
  private final ApplicationServer fServer;
  private final String fId;
  private boolean fBlockRefresh;
  private boolean fMarkReadOnExpand = true;
  private boolean fMarkReadOnScrolling = true;
  private int fPageSize;
  private final IModelFactory fFactory;
  private final IPreferenceScope fPreferences = Owl.getPreferenceService().getGlobalScope();
  private final INewsDAO fNewsDao = DynamicDAO.getDAO(INewsDAO.class);
  private final JobTracker fUserInteractionTracker = new JobTracker(USER_INTERACTION_DELAY, false, true, Priority.INTERACTIVE);
  private final Set<Long> fMarkedUnreadByUserCache = Collections.synchronizedSet(new HashSet<Long>());

  /* This viewer's sorter. <code>null</code> means there is no sorter. */
  private ViewerComparator fSorter;

  /* This viewer's filters (element type: <code>ViewerFilter</code>). */
  private List<ViewerFilter> fFilters;
  private NewsFilter fNewsFilter;

  /* A model of what is displayed in the browser */
  private final NewsBrowserViewModel fViewModel;

  /* Special Element that denotes a Paging Latch */
  static final class PageLatch {}

  /* Task to perform some news related actions based on user interaction */
  final class UserInteractionTask implements ITask {
    private final NewsBrowserViewModel fViewModel;
    private final CBrowser fCBrowser;

    public UserInteractionTask(NewsBrowserViewModel model, CBrowser browser) {
      fViewModel = model;
      fCBrowser = browser;
    }

    public IStatus run(IProgressMonitor monitor) {

      /* Return early if canceled or disposed */
      if (monitor.isCanceled() || fCBrowser.getControl().isDisposed())
        return Status.OK_STATUS;

      /* Reveal next page as necessary */
      if (fPageSize != 0 && fViewModel.hasHiddenNews()) {
        long lastVisibleNewsId = fViewModel.getLastVisibleNews();
        if (lastVisibleNewsId != -1) {
          StringBuilder js = new StringBuilder();
          if (fCBrowser.isIE()) {
            js.append("var scrollPosY = document.body.scrollTop; "); //$NON-NLS-1$
            js.append("var windowHeight = document.body.clientHeight; "); //$NON-NLS-1$
          } else {
            js.append("var scrollPosY = window.pageYOffset; "); //$NON-NLS-1$
            js.append("var windowHeight = window.innerHeight; "); //$NON-NLS-1$
          }

          js.append("if (scrollPosY > 0) {"); //$NON-NLS-1$
          js.append("  var node = document.getElementById('").append(Dynamic.NEWS.getId(lastVisibleNewsId)).append("'); "); //$NON-NLS-1$//$NON-NLS-2$
          js.append("  if (node) {"); //$NON-NLS-1$
          js.append("    if ((scrollPosY + windowHeight) >= node.offsetTop) {"); //$NON-NLS-1$
          js.append("      window.location.href = '").append(ILinkHandler.HANDLER_PROTOCOL + SCROLL_NEXT_PAGE_HANDLER_ID).append("'; "); //$NON-NLS-1$ //$NON-NLS-2$
          js.append("    }"); //$NON-NLS-1$
          js.append("  }"); //$NON-NLS-1$
          js.append("}"); //$NON-NLS-1$

          if (!monitor.isCanceled() && !fCBrowser.getControl().isDisposed())
            fCBrowser.execute(js.toString(), "UserInteractionTask#0"); //$NON-NLS-1$
        }
      }

      /* Return early if canceled or disposed */
      if (monitor.isCanceled() || fCBrowser.getControl().isDisposed())
        return Status.OK_STATUS;

      /* Mark Seen News as Read if necessary */
      if (fMarkReadOnScrolling && !isGroupingByState()) {//Ignore if grouping by state to avoid refresh
        StringBuilder js = new StringBuilder();
        if (fCBrowser.isIE()) {
          js.append("var scrollPosY = document.body.scrollTop; "); //$NON-NLS-1$
          js.append("var windowHeight = document.body.clientHeight; "); //$NON-NLS-1$
        } else {
          js.append("var scrollPosY = window.pageYOffset; "); //$NON-NLS-1$
          js.append("var windowHeight = window.innerHeight; "); //$NON-NLS-1$
        }

        boolean varDefined = false;
        List<Long> visibleUnreadNews = fViewModel.getVisibleUnreadNews();
        long lastNews = fViewModel.getLastNews();
        js.append("var lastNews = document.getElementById('").append(Dynamic.NEWS.getId(lastNews)).append("'); "); //$NON-NLS-1$//$NON-NLS-2$
        js.append("if (lastNews) {"); //$NON-NLS-1$
        js.append("  var newsIds = ''; "); //$NON-NLS-1$
        js.append("  var lastNewsPosY = lastNews.offsetTop; "); //$NON-NLS-1$
        js.append("  var lastNewsHeight = lastNews.offsetHeight; "); //$NON-NLS-1$
        for (Long id : visibleUnreadNews) {
          if (fMarkedUnreadByUserCache.contains(id))
            continue; //Skip those news explicitly marked as unread by the user

          if (!varDefined) {
            js.append("var "); //$NON-NLS-1$
            varDefined = true;
          }

          /*
           * Conditions under which a news gets marked as read:
           *
           * "divPosY < scrollPosY" : Top Border of News is above top scroll position
           * "lastNewsPosY < scrollPosY + windowHeight" : Last news is visible
           */
          js.append("node = document.getElementById('").append(Dynamic.NEWS.getId(id)).append("'); "); //$NON-NLS-1$//$NON-NLS-2$
          js.append("  if (node) {"); //$NON-NLS-1$
          js.append("    var divPosY = node.offsetTop; "); //$NON-NLS-1$
          js.append("    var divHeight = node.offsetHeight; "); //$NON-NLS-1$
          js.append("    if (divPosY < scrollPosY || (lastNewsPosY > 0 && lastNewsPosY < scrollPosY + windowHeight)) {"); //$NON-NLS-1$
          js.append("      newsIds = newsIds + '").append(id).append(",'; "); //$NON-NLS-1$ //$NON-NLS-2$
          js.append("    }"); //$NON-NLS-1$
          js.append("  }"); //$NON-NLS-1$
        }

        js.append("  if (newsIds.length != 0) { "); //$NON-NLS-1$
        js.append("    window.location.href = '").append(ILinkHandler.HANDLER_PROTOCOL + MARK_READ_HANDLER_ID + "?").append("' + newsIds; "); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
        js.append("  } "); //$NON-NLS-1$
        js.append("}"); //$NON-NLS-1$

        if (!monitor.isCanceled() && !fCBrowser.getControl().isDisposed())
          fCBrowser.execute(js.toString(), "UserInteractionTask#1"); //$NON-NLS-1$
      }

      return Status.OK_STATUS;
    }

    public String getName() {
      return null;
    }

    public Priority getPriority() {
      return Priority.INTERACTIVE;
    }
  }

  /**
   * @param parent the parent {@link Composite} to host this viewer.
   * @param style the style of the {@link Browser} in this viewer.
   */
  public NewsBrowserViewer(Composite parent, int style) {
    this(parent, style, null);
  }

  /**
   * @param parent the parent {@link Composite} to host this viewer.
   * @param style the style of the {@link Browser} in this viewer.
   * @param site the {@link IFeedViewSite} if this viewer is being used from a
   * {@link FeedView} or <code>null</code> otherwise.
   */
  public NewsBrowserViewer(Composite parent, int style, IFeedViewSite site) {
    fBrowser = new CBrowser(parent, style);
    fBrowser.setCanOpenLinksInTabs(true);
    fViewModel = new NewsBrowserViewModel(this);
    fSite = site;
    fIsEmbedded = (fSite != null);
    hookControl(fBrowser.getControl());
    hookNewsContextMenu();
    hookAttachmentsContextMenu();
    hookLabelContextMenu();
    hookShareNewsContextMenu();
    hookFindRelatedContextMenu();
    fId = String.valueOf(hashCode());
    fServer = ApplicationServer.getDefault();
    fServer.register(fId, this);
    fFactory = Owl.getModelFactory();

    /* Register Link Handler */
    fBrowser.addLinkHandler(MARK_READ_HANDLER_ID, this);
    fBrowser.addLinkHandler(TOGGLE_READ_HANDLER_ID, this);
    fBrowser.addLinkHandler(TOGGLE_STICKY_HANDLER_ID, this);
    fBrowser.addLinkHandler(ARCHIVE_HANDLER_ID, this);
    fBrowser.addLinkHandler(DELETE_HANDLER_ID, this);
    fBrowser.addLinkHandler(ATTACHMENT_HANDLER_ID, this);
    fBrowser.addLinkHandler(ATTACHMENTS_MENU_HANDLER_ID, this);
    fBrowser.addLinkHandler(LABELS_MENU_HANDLER_ID, this);
    fBrowser.addLinkHandler(EXPAND_NEWS_HANDLER_ID, this);
    fBrowser.addLinkHandler(COLLAPSE_NEWS_HANDLER_ID, this);
    fBrowser.addLinkHandler(EXPAND_GROUP_HANDLER_ID, this);
    fBrowser.addLinkHandler(COLLAPSE_GROUP_HANDLER_ID, this);
    fBrowser.addLinkHandler(GROUP_MENU_HANDLER_ID, this);
    fBrowser.addLinkHandler(NEWS_MENU_HANDLER_ID, this);
    fBrowser.addLinkHandler(SHARE_NEWS_MENU_HANDLER_ID, this);
    fBrowser.addLinkHandler(NEXT_NEWS_HANDLER_ID, this);
    fBrowser.addLinkHandler(NEXT_UNREAD_NEWS_HANDLER_ID, this);
    fBrowser.addLinkHandler(PREVIOUS_NEWS_HANDLER_ID, this);
    fBrowser.addLinkHandler(PREVIOUS_UNREAD_NEWS_HANDLER_ID, this);
    fBrowser.addLinkHandler(TRANSFORM_HANDLER_ID, this);
    fBrowser.addLinkHandler(RELATED_NEWS_MENU_HANDLER_ID, this);
    fBrowser.addLinkHandler(NEXT_PAGE_HANDLER_ID, this);
    fBrowser.addLinkHandler(SCROLL_NEXT_PAGE_HANDLER_ID, this);

    /* React on User Interaction (Mouse Scrolling, Mouse Down, Key Pressed) */
    Listener listener = new Listener() {
      public void handleEvent(Event event) {
        onUserInteraction();
      }
    };
    fBrowser.getControl().addListener(SWT.MouseWheel, listener);
    fBrowser.getControl().addListener(SWT.MouseDown, listener);
    fBrowser.getControl().addListener(SWT.KeyDown, listener);
  }

  private void onUserInteraction() {

    /* Return if feature not necessary at all */
    if (!fIsEmbedded || (!fMarkReadOnScrolling && fPageSize == 0))
      return;

    /* Return if disposed or already running */
    if (fBrowser.getControl().isDisposed() || fUserInteractionTracker.isRunning())
      return;

    /* Tell the tracker about user interaction*/
    fUserInteractionTracker.run(new UserInteractionTask(fViewModel, fBrowser));
  }

  private void hookNewsContextMenu() {
    MenuManager manager = new MenuManager();
    manager.setRemoveAllWhenShown(true);
    manager.addMenuListener(new IMenuListener() {
      @SuppressWarnings("restriction")
      public void menuAboutToShow(IMenuManager manager) {

        /* Open */
        {
          boolean useSeparator = true;

          /* Open in FeedView */
          if (!fIsEmbedded) {
            manager.add(new Separator("internalopen")); //$NON-NLS-1$
            if (!fCurrentSelection.isEmpty()) {
              manager.appendToGroup("internalopen", new OpenNewsAction(fCurrentSelection, fBrowser.getControl().getShell())); //$NON-NLS-1$
              useSeparator = false;
            }
          }

          manager.add(useSeparator ? new Separator("open") : new GroupMarker("open")); //$NON-NLS-1$ //$NON-NLS-2$

          /* Show only when internal browser is used */
          if (!fCurrentSelection.isEmpty() && !OwlUI.useExternalBrowser())
            manager.add(new OpenInExternalBrowserAction(fCurrentSelection));
        }

        /* Attachments */
        {
          ApplicationActionBarAdvisor.fillAttachmentsMenu(manager, fCurrentSelection, new SameShellProvider(fBrowser.getControl().getShell()), false);
        }

        /* Mark / Label */
        {
          manager.add(new Separator("mark")); //$NON-NLS-1$

          /* Mark */
          MenuManager markMenu = new MenuManager(Messages.NewsBrowserViewer_MARK, "mark"); //$NON-NLS-1$
          manager.add(markMenu);

          /* Mark as Read */
          IAction action = new ToggleReadStateAction(fCurrentSelection);
          action.setEnabled(!fCurrentSelection.isEmpty());
          markMenu.add(action);

          /* Mark All Read */
          action = new MarkAllNewsReadAction();
          markMenu.add(action);

          /* Sticky */
          markMenu.add(new Separator());
          action = new MakeNewsStickyAction(fCurrentSelection);
          action.setEnabled(!fCurrentSelection.isEmpty());
          markMenu.add(action);

          /* Label */
          ApplicationActionBarAdvisor.fillLabelMenu(manager, fCurrentSelection, new SameShellProvider(fBrowser.getControl().getShell()), false);
        }

        /* Move To / Copy To */
        if (!fCurrentSelection.isEmpty()) {
          manager.add(new Separator("movecopy")); //$NON-NLS-1$

          /* Load all news bins and sort by name */
          List<INewsBin> newsbins = new ArrayList<INewsBin>(DynamicDAO.loadAll(INewsBin.class));

          Comparator<INewsBin> comparator = new Comparator<INewsBin>() {
            public int compare(INewsBin o1, INewsBin o2) {
              return o1.getName().compareTo(o2.getName());
            };
          };

          Collections.sort(newsbins, comparator);

          /* Move To */
          MenuManager moveMenu = new MenuManager(Messages.NewsBrowserViewer_MOVE_TO, "moveto"); //$NON-NLS-1$
          manager.add(moveMenu);

          for (INewsBin bin : newsbins) {
            if (contained(bin, fCurrentSelection))
              continue;

            moveMenu.add(new MoveCopyNewsToBinAction(fCurrentSelection, bin, true));
          }

          moveMenu.add(new MoveCopyNewsToBinAction(fCurrentSelection, null, true));
          moveMenu.add(new Separator());
          moveMenu.add(new AutomateFilterAction(PresetAction.MOVE, fCurrentSelection));

          /* Copy To */
          MenuManager copyMenu = new MenuManager(Messages.NewsBrowserViewer_COPY_TO, "copyto"); //$NON-NLS-1$
          manager.add(copyMenu);

          for (INewsBin bin : newsbins) {
            if (contained(bin, fCurrentSelection))
              continue;

            copyMenu.add(new MoveCopyNewsToBinAction(fCurrentSelection, bin, false));
          }

          copyMenu.add(new MoveCopyNewsToBinAction(fCurrentSelection, null, false));
          copyMenu.add(new Separator());
          copyMenu.add(new AutomateFilterAction(PresetAction.COPY, fCurrentSelection));

          /* Archive */
          manager.add(new ArchiveNewsAction(fCurrentSelection));
        }

        /* Share */
        boolean entityGroupSelected = ModelUtils.isEntityGroupSelected(fCurrentSelection);
        if (!entityGroupSelected)
          ApplicationActionBarAdvisor.fillShareMenu(manager, fCurrentSelection, new SameShellProvider(fBrowser.getControl().getShell()), false);

        manager.add(new Separator("filter")); //$NON-NLS-1$
        manager.add(new Separator("copy")); //$NON-NLS-1$
        manager.add(new GroupMarker("edit")); //$NON-NLS-1$

        /* Collapse All */
        if (entityGroupSelected) {
          manager.add(new Separator());
          ImageDescriptor icon = OwlUI.getImageDescriptor("icons/etool16/collapseall.gif"); //$NON-NLS-1$
          manager.add(new Action(Messages.NewsBrowserViewer_COLLAPSE_GROUPS, icon) {
            @Override
            public void run() {
              Set<Entry<Long, List<Long>>> groups = fViewModel.getGroups().entrySet();
              for (Entry<Long, List<Long>> group : groups) {
                Long groupId = group.getKey();
                if (fViewModel.isGroupVisible(groupId)) {
                  List<Long> newsIds = group.getValue();
                  if (newsIds != null && !newsIds.isEmpty())
                    setGroupExpanded(groupId, newsIds, false);
                }
              }
            };
          });
        }

        manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));

        /* Fill Contributions if Context Menu not registered */
        if (fSite == null)
          org.eclipse.ui.internal.ObjectActionContributorManager.getManager().contributeObjectActions(null, manager, NewsBrowserViewer.this);
      }
    });

    /* Create and Register with Workbench */
    fNewsContextMenu = manager.createContextMenu(fBrowser.getControl().getShell());

    /* Register with Part Site if possible */
    if (fSite != null)
      fSite.getEditorSite().registerContextMenu(manager, this);
  }

  private void hookAttachmentsContextMenu() {
    MenuManager manager = new MenuManager();
    manager.setRemoveAllWhenShown(true);
    manager.addMenuListener(new IMenuListener() {
      public void menuAboutToShow(IMenuManager manager) {
        ApplicationActionBarAdvisor.fillAttachmentsMenu(manager, fCurrentSelection, new SameShellProvider(fBrowser.getControl().getShell()), true);
      }
    });

    /* Create  */
    fAttachmentsContextMenu = manager.createContextMenu(fBrowser.getControl().getShell());
  }

  private void hookLabelContextMenu() {
    MenuManager manager = new MenuManager();
    manager.setRemoveAllWhenShown(true);
    manager.addMenuListener(new IMenuListener() {
      public void menuAboutToShow(IMenuManager manager) {
        ApplicationActionBarAdvisor.fillLabelMenu(manager, fCurrentSelection, new SameShellProvider(fBrowser.getControl().getShell()), true);
      }
    });

    /* Create  */
    fLabelsContextMenu = manager.createContextMenu(fBrowser.getControl().getShell());
  }

  private void hookShareNewsContextMenu() {
    MenuManager manager = new MenuManager();
    manager.setRemoveAllWhenShown(true);
    manager.addMenuListener(new IMenuListener() {
      public void menuAboutToShow(IMenuManager manager) {
        ApplicationActionBarAdvisor.fillShareMenu(manager, fCurrentSelection, new SameShellProvider(fBrowser.getControl().getShell()), true);
      }
    });

    /* Create  */
    fShareNewsContextMenu = manager.createContextMenu(fBrowser.getControl().getShell());
  }

  private void hookFindRelatedContextMenu() {
    MenuManager manager = new MenuManager();
    manager.setRemoveAllWhenShown(true);
    manager.addMenuListener(new IMenuListener() {
      public void menuAboutToShow(IMenuManager manager) {
        if (fCurrentSelection.size() == 1) {
          Object element = fCurrentSelection.getFirstElement();
          if (element instanceof INews) {
            final INews news = (INews) element;
            final String entity = INews.class.getName();

            /* Find Related by Title */
            manager.add(new Action(Messages.NewsBrowserViewer_SIMILAR_CONTENT) {
              @Override
              public void run() {
                List<ISearchCondition> conditions = new ArrayList<ISearchCondition>(1);
                String headline = CoreUtils.getHeadline(news, false);

                ISearchField field = fFactory.createSearchField(IEntity.ALL_FIELDS, entity);
                ISearchCondition condition = fFactory.createSearchCondition(field, SearchSpecifier.CONTAINS, headline);
                conditions.add(condition);

                doSearch(conditions, true);
              };
            });

            /* Find Related by Author */
            if (news.getAuthor() != null) {
              IPerson person = news.getAuthor();
              String name = person.getName();
              String email = (person.getEmail() != null) ? person.getEmail().toASCIIString() : null;

              final String author = StringUtils.isSet(name) ? name : email;
              if (StringUtils.isSet(author)) {
                manager.add(new Separator());
                manager.add(new Action(NLS.bind(Messages.NewsBrowserViewer_AUTHORED_BY, escapeForMenu(author))) {
                  @Override
                  public void run() {
                    List<ISearchCondition> conditions = new ArrayList<ISearchCondition>(1);

                    ISearchField field = fFactory.createSearchField(INews.AUTHOR, entity);
                    ISearchCondition condition = fFactory.createSearchCondition(field, SearchSpecifier.CONTAINS_ALL, author);
                    conditions.add(condition);

                    doSearch(conditions, false);
                  };
                });
              }
            }

            /* Find Related by Category */
            if (!news.getCategories().isEmpty()) {

              /* Directly show for one category */
              if (news.getCategories().size() == 1) {
                final String name = news.getCategories().get(0).getName();
                if (StringUtils.isSet(name)) {
                  manager.add(new Action(NLS.bind(Messages.NewsBrowserViewer_CATEGORIZED_N, escapeForMenu(name))) {
                    @Override
                    public void run() {
                      List<ISearchCondition> conditions = new ArrayList<ISearchCondition>(1);

                      ISearchField field = fFactory.createSearchField(INews.CATEGORIES, entity);
                      ISearchCondition condition = fFactory.createSearchCondition(field, SearchSpecifier.IS, name);
                      conditions.add(condition);

                      doSearch(conditions, false);
                    };
                  });
                }
              }

              /* Use a Sub Menu for many categories */
              else {
                MenuManager categoriesMenu = new MenuManager(Messages.NewsBrowserViewer_BY_CATEGORY);
                for (ICategory category : news.getCategories()) {
                  final String name = category.getName();
                  if (StringUtils.isSet(name)) {
                    categoriesMenu.add(new Action(escapeForMenu(name)) {
                      @Override
                      public void run() {
                        List<ISearchCondition> conditions = new ArrayList<ISearchCondition>(1);

                        ISearchField field = fFactory.createSearchField(INews.CATEGORIES, entity);
                        ISearchCondition condition = fFactory.createSearchCondition(field, SearchSpecifier.IS, name);
                        conditions.add(condition);

                        doSearch(conditions, false);
                      };
                    });
                  }
                }
                manager.add(categoriesMenu);
              }
            }

            /* Find Related by Labels */
            if (!news.getLabels().isEmpty()) {
              manager.add(new Separator());
              for (final ILabel label : news.getLabels()) {
                manager.add(new Action(NLS.bind(Messages.NewsBrowserViewer_LABELED_N, escapeForMenu(label.getName()))) {
                  @Override
                  public void run() {
                    List<ISearchCondition> conditions = new ArrayList<ISearchCondition>(1);

                    ISearchField field = fFactory.createSearchField(INews.LABEL, entity);
                    ISearchCondition condition = fFactory.createSearchCondition(field, SearchSpecifier.IS, label.getName());
                    conditions.add(condition);

                    doSearch(conditions, false);
                  };
                });
              }
            }
          }
        }
      }
    });

    /* Create  */
    fFindRelatedContextMenu = manager.createContextMenu(fBrowser.getControl().getShell());
  }

  private String escapeForMenu(String str) {
    return StringUtils.replaceAll(str, "&", "&&"); //$NON-NLS-1$ //$NON-NLS-2$
  }

  private void doSearch(final List<ISearchCondition> conditions, final boolean useLowScoreFilter) {
    if (conditions.size() >= 1 && !fBrowser.getControl().isDisposed()) {

      /* See Bug 747 - run asynced */
      delayInUI(new Runnable() {
        public void run() {
          SearchNewsDialog dialog = new SearchNewsDialog(fBrowser.getControl().getShell(), conditions, true, true);
          dialog.setUseLowScoreFilter(useLowScoreFilter);
          dialog.open();
        }
      });
    }
  }

  private boolean contained(INewsBin bin, IStructuredSelection selection) {
    if (selection == null || selection.isEmpty())
      return false;

    Object element = selection.getFirstElement();
    if (element instanceof INews) {
      INews news = (INews) element;
      return news.getParentId() == bin.getId();
    }

    return false;
  }

  void setBlockRefresh(boolean block) {
    fBlockRefresh = block;
  }

  /*
   * @see org.rssowl.ui.internal.ILinkHandler#handle(java.lang.String, java.net.URI)
   */
  public void handle(final String id, URI link) {

    /* Extract Query Part and Decode */
    String query = link.getQuery();
    boolean queryProvided = StringUtils.isSet(query);
    if (queryProvided) {
      query = URIUtils.urlDecode(query).trim();
      queryProvided = StringUtils.isSet(query);
    }

    /* Mark Read */
    if (queryProvided && MARK_READ_HANDLER_ID.equals(id)) {
      final List<INews> news = getNewsList(query);
      Runnable runnable = new Runnable() {
        public void run() {
          INews.State newState = INews.State.READ;
          boolean affectEquivalentNews = OwlUI.markReadDuplicates();
          UndoStack.getInstance().addOperation(new NewsStateOperation(news, newState, affectEquivalentNews));
          fNewsDao.setState(news, newState, affectEquivalentNews, false);
        }
      };

      if (CBrowser.isMozillaRunningOnWindows()) //Bug in XULRunner, otherwise won't work
        delayInUI(runnable);
      else
        runnable.run();
    }

    /*  Toggle Read */
    else if (queryProvided && TOGGLE_READ_HANDLER_ID.equals(id)) {
      INews news = getNews(query);
      if (news != null) {

        /* Remove Focus from Link */
        blur(Dynamic.TOGGLE_READ_LINK.getId(news));

        /* Update State */
        INews.State newState = (news.getState() == INews.State.READ) ? INews.State.UNREAD : INews.State.READ;
        Set<INews> singleNewsSet = Collections.singleton(news);
        boolean affectEquivalentNews = (newState != INews.State.UNREAD && OwlUI.markReadDuplicates());
        UndoStack.getInstance().addOperation(new NewsStateOperation(singleNewsSet, newState, affectEquivalentNews));
        fNewsDao.setState(singleNewsSet, newState, affectEquivalentNews, false);
        if (newState == INews.State.UNREAD)
          fMarkedUnreadByUserCache.add(news.getId());
      }
    }

    /*  Toggle Sticky */
    else if (queryProvided && TOGGLE_STICKY_HANDLER_ID.equals(id)) {
      INews news = getNews(query);
      if (news != null) {

        /* Remove Focus from Link */
        blur(Dynamic.TOGGLE_STICKY_LINK.getId(news));
        if (isHeadlinesLayout())
          blur(Dynamic.TINY_TOGGLE_STICKY_LINK.getId(news), true);

        /* Toggle Sticky State */
        Set<INews> singleNewsSet = Collections.singleton(news);
        UndoStack.getInstance().addOperation(new StickyOperation(singleNewsSet, !news.isFlagged()));
        news.setFlagged(!news.isFlagged());
        Controller.getDefault().getSavedSearchService().forceQuickUpdate();
        DynamicDAO.saveAll(singleNewsSet);
      }
    }

    /*  Archive */
    else if (queryProvided && ARCHIVE_HANDLER_ID.equals(id)) {
      INews news = getNews(query);
      if (news != null) {
        ArchiveNewsAction action = new ArchiveNewsAction(new StructuredSelection(news));
        action.run();
      }
    }

    /*  Delete */
    else if (queryProvided && DELETE_HANDLER_ID.equals(id)) {
      INews news = getNews(query);
      if (news != null) {
        Set<INews> singleNewsSet = Collections.singleton(news);
        UndoStack.getInstance().addOperation(new NewsStateOperation(singleNewsSet, INews.State.HIDDEN, false));
        fNewsDao.setState(singleNewsSet, INews.State.HIDDEN, false, false);
      }
    }

    /*  Labels Menu */
    else if (queryProvided && LABELS_MENU_HANDLER_ID.equals(id)) {
      INews news = getNews(query);
      if (news != null) {

        /* Remove Focus from Link */
        blur(Dynamic.LABELS_MENU_LINK.getId(news));

        /* Show Menu */
        setSelection(new StructuredSelection(news));
        Point cursorLocation = fBrowser.getControl().getDisplay().getCursorLocation();
        cursorLocation.y = cursorLocation.y + 16;
        fLabelsContextMenu.setLocation(cursorLocation);
        fLabelsContextMenu.setVisible(true);
      }
    }

    /*  Attachments Menu */
    else if (queryProvided && (ATTACHMENTS_MENU_HANDLER_ID.equals(id) || ATTACHMENT_HANDLER_ID.equals(id))) {
      INews news = getNews(query);
      if (news != null) {

        /* Remove Focus from Link */
        if (ATTACHMENT_HANDLER_ID.equals(id))
          blur(Dynamic.ATTACHMENT_LINK.getId(news));
        else if (ATTACHMENTS_MENU_HANDLER_ID.equals(id))
          blur(Dynamic.ATTACHMENTS_MENU_LINK.getId(news));

        /* Show Menu */
        setSelection(new StructuredSelection(news));
        Point cursorLocation = fBrowser.getControl().getDisplay().getCursorLocation();
        cursorLocation.y = cursorLocation.y + 16;
        fAttachmentsContextMenu.setLocation(cursorLocation);
        fAttachmentsContextMenu.setVisible(true);
      }
    }

    /* Toggle News Item Visibility */
    else if (queryProvided && (EXPAND_NEWS_HANDLER_ID.equals(id) || COLLAPSE_NEWS_HANDLER_ID.equals(id))) {
      INews news = getNews(query);
      if (news != null) {
        setNewsExpanded(news, EXPAND_NEWS_HANDLER_ID.equals(id));
        onUserInteraction();
      }
    }

    /* Toggle Group Items Visibility */
    else if (queryProvided && (EXPAND_GROUP_HANDLER_ID.equals(id) || COLLAPSE_GROUP_HANDLER_ID.equals(id))) {
      long groupId = getId(query);
      List<Long> newsIds = fViewModel.getNewsIds(groupId);
      if (!newsIds.isEmpty())
        setGroupExpanded(groupId, newsIds, EXPAND_GROUP_HANDLER_ID.equals(id));
    }

    /* Group Context Menu */
    else if (queryProvided && GROUP_MENU_HANDLER_ID.equals(id)) {
      EntityGroup group = getEntityGroup(query);
      if (group != null) {

        /* Remove Focus from Link */
        blur(Dynamic.GROUP_MENU_LINK.getId(group));

        /* Show Menu */
        setSelection(new StructuredSelection(group));
        Point cursorLocation = fBrowser.getControl().getDisplay().getCursorLocation();
        cursorLocation.y = cursorLocation.y + 16;
        fNewsContextMenu.setLocation(cursorLocation);
        fNewsContextMenu.setVisible(true);
      }
    }

    /* News Context Menu */
    else if (queryProvided && NEWS_MENU_HANDLER_ID.equals(id)) {
      INews news = getNews(query);
      if (news != null) {

        /* Remove Focus from Link */
        blur(Dynamic.NEWS_MENU_LINK.getId(news), true);
        blur(Dynamic.FOOTER_NEWS_MENU_LINK.getId(news), true);

        /* Show Menu */
        setSelection(new StructuredSelection(news));
        Point cursorLocation = fBrowser.getControl().getDisplay().getCursorLocation();
        cursorLocation.y = cursorLocation.y + 16;
        fNewsContextMenu.setLocation(cursorLocation);
        fNewsContextMenu.setVisible(true);
      }
    }

    /* Share News Context Menu */
    else if (queryProvided && SHARE_NEWS_MENU_HANDLER_ID.equals(id)) {
      INews news = getNews(query);
      if (news != null) {

        /* Remove Focus from Link */
        blur(Dynamic.SHARE_MENU_LINK.getId(news));

        /* Show Menu */
        setSelection(new StructuredSelection(news));
        Point cursorLocation = fBrowser.getControl().getDisplay().getCursorLocation();
        cursorLocation.y = cursorLocation.y + 16;
        fShareNewsContextMenu.setLocation(cursorLocation);
        fShareNewsContextMenu.setVisible(true);
      }
    }

    /* Find Related Context Menu */
    else if (queryProvided && RELATED_NEWS_MENU_HANDLER_ID.equals(id)) {
      INews news = getNews(query);
      if (news != null) {

        /* Remove Focus from Link */
        blur(Dynamic.FIND_RELATED_MENU_LINK.getId(news));

        /* Show Menu */
        setSelection(new StructuredSelection(news));
        Point cursorLocation = fBrowser.getControl().getDisplay().getCursorLocation();
        cursorLocation.y = cursorLocation.y + 16;
        fFindRelatedContextMenu.setLocation(cursorLocation);
        fFindRelatedContextMenu.setVisible(true);
      }
    }

    /* Go to Next News / Go to Next Unread News / Go to Previous News / Go to Previous Unread News */
    else if (NEXT_NEWS_HANDLER_ID.equals(id) || NEXT_UNREAD_NEWS_HANDLER_ID.equals(id) || PREVIOUS_NEWS_HANDLER_ID.equals(id) || PREVIOUS_UNREAD_NEWS_HANDLER_ID.equals(id)) {
      Runnable runnable = new Runnable() {
        public void run() {
          handleNavigateAction(id);
        }
      };

      if (CBrowser.isMozillaRunningOnWindows()) //Bug in XULRunner, otherwise won't work
        delayInUI(runnable);
      else
        runnable.run();
    }

    /* Transform News */
    else if (TRANSFORM_HANDLER_ID.equals(id)) {
      INews news = getNews(query);
      if (news != null)
        transformNews(news);
    }

    /* Reveal Next Page */
    else if (NEXT_PAGE_HANDLER_ID.equals(id)) {
      revealNextPage(true);
    }

    /* Scroll Reveal Next Page */
    else if (SCROLL_NEXT_PAGE_HANDLER_ID.equals(id)) {
      Runnable runnable = new Runnable() {
        public void run() {
          revealNextPage(false);
        }
      };

      if (CBrowser.isMozillaRunningOnWindows()) //Bug in XULRunner, otherwise won't work
        delayInUI(runnable);
      else
        runnable.run();
    }
  }

  private void handleNavigateAction(final String id) {

    /* Special Case Navigation in Newspaper mode when some news are hidden from the page */
    if (!isHeadlinesLayout() && fPageSize != 0 && (NEXT_NEWS_HANDLER_ID.equals(id) || NEXT_UNREAD_NEWS_HANDLER_ID.equals(id))) {
      boolean onlyUnread = NEXT_UNREAD_NEWS_HANDLER_ID.equals(id);
      int totalNewsCount = fViewModel.getNewsCount();
      int visibleNewsCount = fViewModel.getVisibleNewsCount();

      /* There are hidden News beyond the Page Latch */
      if (totalNewsCount != 0 && totalNewsCount > visibleNewsCount) {
        long firstHiddenNewsId = fViewModel.getFirstHiddenNews(onlyUnread);
        if (firstHiddenNewsId != -1) {
          showSelection(new StructuredSelection(new NewsReference(firstHiddenNewsId)));
          return;
        }
      }
    }

    /* Forward the navigation action to the outer scope */
    delayInUI(new Runnable() {
      public void run() {
        NavigationActionFactory factory = new NavigationActionFactory();
        try {
          NavigationActionFactory.NavigationActionType type = null;
          if (NEXT_NEWS_HANDLER_ID.equals(id))
            type = NavigationActionFactory.NavigationActionType.NEXT_FEED_NEXT_NEWS;
          else if (NEXT_UNREAD_NEWS_HANDLER_ID.equals(id))
            type = NavigationActionFactory.NavigationActionType.NEXT_UNREAD_FEED_NEXT_UNREAD_NEWS;
          else if (PREVIOUS_NEWS_HANDLER_ID.equals(id))
            type = NavigationActionFactory.NavigationActionType.PREVIOUS_FEED_PREVIOUS_NEWS;
          else if (PREVIOUS_UNREAD_NEWS_HANDLER_ID.equals(id))
            type = NavigationActionFactory.NavigationActionType.PREVIOUS_UNREAD_FEED_PREVIOUS_UNREAD_NEWS;

          if (type != null) {
            factory.setInitializationData(null, null, type.getId());
            IWorkbenchWindowActionDelegate action = (IWorkbenchWindowActionDelegate) factory.create();
            action.run(null);
          }
        } catch (CoreException e) {
          /* Ignore */
        }
      }
    });
  }

  private void setNewsExpanded(INews news, boolean expanded) {
    setNewsExpanded(news, expanded, true);
  }

  private void setNewsExpanded(INews news, boolean expanded, boolean scrollIntoView) {

    /* Return early if visibility already matches state */
    if (expanded == fViewModel.isNewsExpanded(news))
      return;

    /* Link and Image */
    final StringBuilder js = new StringBuilder();
    String newsLink = CoreUtils.getLink(news);

    /* Blur Links */
    js.append(getElementById(Dynamic.TITLE_LINK.getId(news)).append(".blur(); ")); //$NON-NLS-1$

    /* Update Links */
    String link = HANDLER_PROTOCOL + (expanded ? COLLAPSE_NEWS_HANDLER_ID : EXPAND_NEWS_HANDLER_ID) + "?" + news.getId(); //$NON-NLS-1$
    if (expanded && StringUtils.isSet(newsLink))
      js.append(getElementById(Dynamic.TITLE_LINK.getId(news)).append(".href = '" + URIUtils.toManaged(newsLink) + "'; ")); //$NON-NLS-1$ //$NON-NLS-2$
    else
      js.append(getElementById(Dynamic.TITLE_LINK.getId(news)).append(".href='").append(link).append("'; ")); //$NON-NLS-1$ //$NON-NLS-2$

    /* Update Toggle Sticky Link Visibility */
    if (expanded)
      js.append(getElementById(Dynamic.TINY_TOGGLE_STICKY_LINK.getId(news))).append(".style.display='none'; "); //$NON-NLS-1$
    else
      js.append(getElementById(Dynamic.TINY_TOGGLE_STICKY_LINK.getId(news))).append(".style.display='inline'; "); //$NON-NLS-1$

    /* Update Subtitle if present */
    if (expanded)
      js.append(getElementById(Dynamic.SUBTITLE_LINK.getId(news))).append(".style.display='none'; "); //$NON-NLS-1$
    else {
      StringBuilder subtitleContent = new StringBuilder();
      IBaseLabelProvider lp = getLabelProvider();
      if (lp instanceof NewsBrowserLabelProvider)
        ((NewsBrowserLabelProvider) lp).fillSubtitle(subtitleContent, news, CoreUtils.getSortedLabels(news), false);
      if (subtitleContent.length() > 0)
        js.append(getElementById(Dynamic.SUBTITLE_LINK.getId(news))).append(".innerHTML='").append(escapeForInnerHtml(subtitleContent.toString())).append("'; "); //$NON-NLS-1$ //$NON-NLS-2$
      js.append(getElementById(Dynamic.SUBTITLE_LINK.getId(news))).append(".style.display='inline'; "); //$NON-NLS-1$
    }

    /* Update News Div Visibility */
    Set<Dynamic> elements = EnumSet.of(Dynamic.SUBLINE, Dynamic.DELETE, Dynamic.CONTENT, Dynamic.FOOTER);
    for (Dynamic element : elements) {
      if (expanded)
        js.append(getElementById(element.getId(news))).append(".style.display='block'; "); //$NON-NLS-1$
      else
        js.append(getElementById(element.getId(news))).append(".style.display='none'; "); //$NON-NLS-1$
    }

    /* Update Headlines Separator if present and parent group (if any) is not collapsed */
    boolean showHeadlinesSeparator = !expanded;
    if (isGroupingEnabled()) {
      long groupId = fViewModel.findGroup(news.getId());
      if (!fViewModel.isGroupExpanded(groupId))
        showHeadlinesSeparator = false;
    }

    js.append("if (").append(getElementById(Dynamic.HEADLINE_SEPARATOR.getId(news))).append(") {"); //$NON-NLS-1$ //$NON-NLS-2$
    if (showHeadlinesSeparator)
      js.append(getElementById(Dynamic.HEADLINE_SEPARATOR.getId(news))).append(".style.display='block'; "); //$NON-NLS-1$
    else
      js.append(getElementById(Dynamic.HEADLINE_SEPARATOR.getId(news))).append(".style.display='none'; "); //$NON-NLS-1$
    js.append("}"); //$NON-NLS-1$

    /* Update Title CSS */
    if (expanded)
      js.append(getElementById(Dynamic.TITLE.getId(news))).append(".className='titleExpanded'; "); //$NON-NLS-1$
    else
      js.append(getElementById(Dynamic.TITLE.getId(news))).append(".className='titleCollapsed'; "); //$NON-NLS-1$

    /* Update News Content as needed */
    if (expanded)
      fillNewsContent(news, js, newsLink);
    else
      js.append(getElementById(Dynamic.CONTENT.getId(news)).append(".innerHTML = ''; ")); //$NON-NLS-1$

    /* Scroll expanded news into view as necessary */
    if (scrollIntoView && expanded)
      scrollIfNecessary(news, js);

    /* Collapse other visible news if present */
    if (expanded) {
      long expandedNewsId = fViewModel.getExpandedNews();
      if (expandedNewsId != -1) {
        INews item = resolve(expandedNewsId);
        if (item != null)
          setNewsExpanded(item, false);
      }
    }

    /* Block external navigation while setting innerHTML */
    fBrowser.blockExternalNavigationWhile(new Runnable() {
      public void run() {
        fBrowser.execute(js.toString(), "setNewsExpanded"); //$NON-NLS-1$
      }
    });

    /* Update State if not already marked as read */
    if (fMarkReadOnExpand && expanded && news.getState() != INews.State.READ && !isGroupingByState()) { //Ignore if grouping by state to avoid refresh
      Set<INews> singleNewsSet = Collections.singleton(news);
      boolean affectEquivalentNews = OwlUI.markReadDuplicates();
      UndoStack.getInstance().addOperation(new NewsStateOperation(singleNewsSet, INews.State.READ, affectEquivalentNews));
      fNewsDao.setState(singleNewsSet, INews.State.READ, affectEquivalentNews, false);
    }

    /* Update Cache of Expanded News */
    fViewModel.setNewsExpanded(news, expanded);
  }

  private void fillNewsContent(INews news, final StringBuilder js, String newsLink) {
    String description = news.getDescription();

    /* Content is provided */
    if (StringUtils.isSet(description) && !description.equals(news.getTitle())) {
      IBaseLabelProvider labelProvider = getLabelProvider();
      if (labelProvider instanceof NewsBrowserLabelProvider) {
        description = ((NewsBrowserLabelProvider) labelProvider).stripMediaTagsIfNecessary(description);
        description = ((NewsBrowserLabelProvider) labelProvider).highlightSearchTermsIfNecessary(description);
      }
    }

    /* Content is not provided */
    else {
      StringBuilder emptyDescription = new StringBuilder();
      emptyDescription.append(Messages.NewsBrowserViewer_NO_CONTENT);
      if (StringUtils.isSet(newsLink)) {
        String link = HANDLER_PROTOCOL + TRANSFORM_HANDLER_ID + "?" + news.getId(); //$NON-NLS-1$
        emptyDescription.append(" <a href=\"").append(link).append("\">").append(Messages.NewsBrowserViewer_DOWNLOAD_CONTENT).append("</a>"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
      }

      description = emptyDescription.toString();
    }

    js.append(getElementById(Dynamic.CONTENT.getId(news)).append(".innerHTML = '" + escapeForInnerHtml(description) + "'; ")); //$NON-NLS-1$ //$NON-NLS-2$
  }

  private void scrollIfNecessary(INews news, final StringBuilder js) {
    if (fBrowser.isIE()) {
      js.append("var scrollPosY = document.body.scrollTop; "); //$NON-NLS-1$
      js.append("var windowHeight = document.body.clientHeight; "); //$NON-NLS-1$
    } else {
      js.append("var scrollPosY = window.pageYOffset; "); //$NON-NLS-1$
      js.append("var windowHeight = window.innerHeight; "); //$NON-NLS-1$
    }

    js.append("var divPosY = ").append(getElementById(Dynamic.NEWS.getId(news))).append(".offsetTop; "); //$NON-NLS-1$ //$NON-NLS-2$
    js.append("var divHeight = ").append(getElementById(Dynamic.NEWS.getId(news))).append(".offsetHeight; "); //$NON-NLS-1$ //$NON-NLS-2$
    js.append("if (scrollPosY > divPosY || divHeight > windowHeight) {"); //$NON-NLS-1$ //Scroll up to reveal the top of the news (also scroll up if the news is larger the client height
    js.append(getElementById(Dynamic.NEWS.getId(news))).append(".scrollIntoView(true); "); //$NON-NLS-1$
    js.append("} else if (scrollPosY + windowHeight < divPosY + divHeight) {"); //$NON-NLS-1$ //Scroll down to reveal the bottom of the news
    js.append(getElementById(Dynamic.NEWS.getId(news))).append(".scrollIntoView(false); "); //$NON-NLS-1$
    js.append("}"); //$NON-NLS-1$
  }

  private void setGroupExpanded(long groupId, List<Long> newsIds, boolean expanded) {
    setGroupExpanded(groupId, newsIds, expanded, true);
  }

  private void setGroupExpanded(long groupId, List<Long> newsIds, boolean expanded, boolean scrollIntoView) {

    /* Image */
    StringBuilder js = new StringBuilder();
    String newToggleImgUri;
    if (fBrowser.isIE())
      newToggleImgUri = expanded ? OwlUI.getImageUri("/icons/elcl16/expanded.gif", "expanded.gif") : OwlUI.getImageUri("/icons/elcl16/collapsed.gif", "collapsed.gif"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
    else
      newToggleImgUri = expanded ? ApplicationServer.getDefault().toResourceUrl("/icons/elcl16/expanded.gif") : ApplicationServer.getDefault().toResourceUrl("/icons/elcl16/collapsed.gif"); //$NON-NLS-1$ //$NON-NLS-2$

    /* Blur Links */
    js.append(getElementById(Dynamic.TOGGLE_GROUP_LINK.getId(groupId)).append(".blur(); ")); //$NON-NLS-1$
    js.append(getElementById(Dynamic.GROUP_MENU_LINK.getId(groupId)).append(".blur(); ")); //$NON-NLS-1$

    /* Update Links */
    String toggleVisibilityLink = HANDLER_PROTOCOL + (expanded ? COLLAPSE_GROUP_HANDLER_ID : EXPAND_GROUP_HANDLER_ID) + "?" + groupId; //$NON-NLS-1$
    js.append(getElementById(Dynamic.TOGGLE_GROUP_LINK.getId(groupId)).append(".href='").append(toggleVisibilityLink).append("'; ")); //$NON-NLS-1$ //$NON-NLS-2$
    if (expanded)
      js.append(getElementById(Dynamic.GROUP_MENU_LINK.getId(groupId)).append(".href='").append(HANDLER_PROTOCOL + GROUP_MENU_HANDLER_ID + "?" + groupId).append("'; ")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
    else
      js.append(getElementById(Dynamic.GROUP_MENU_LINK.getId(groupId)).append(".href='").append(toggleVisibilityLink).append("'; ")); //$NON-NLS-1$ //$NON-NLS-2$

    /* Update Triangle Image */
    js.append(getElementById(Dynamic.TOGGLE_GROUP_IMG.getId(groupId)).append(".src = '" + newToggleImgUri + "'; ")); //$NON-NLS-1$ //$NON-NLS-2$

    /* Update Visibility */
    for (Long id : newsIds) {
      if (!fViewModel.isNewsVisible(id))
        continue; //Skip news if it is actually hidden

      /* News Item */
      if (expanded)
        js.append(getElementById(Dynamic.NEWS.getId(id))).append(".style.display='block'; "); //$NON-NLS-1$
      else
        js.append(getElementById(Dynamic.NEWS.getId(id))).append(".style.display='none'; "); //$NON-NLS-1$

      /* Separator if using headlines layout */
      if (isHeadlinesLayout()) {
        js.append("if (").append(getElementById(Dynamic.HEADLINE_SEPARATOR.getId(id))).append(") {"); //$NON-NLS-1$ //$NON-NLS-2$
        if (expanded)
          js.append(getElementById(Dynamic.HEADLINE_SEPARATOR.getId(id))).append(".style.display='block'; "); //$NON-NLS-1$
        else
          js.append(getElementById(Dynamic.HEADLINE_SEPARATOR.getId(id))).append(".style.display='none'; "); //$NON-NLS-1$
        js.append("}"); //$NON-NLS-1$
      }
    }

    /* Scroll expanded group into view as necessary */
    if (scrollIntoView && expanded && !newsIds.isEmpty()) {
      if (fBrowser.isIE()) {
        js.append("var scrollPosY = document.body.scrollTop; "); //$NON-NLS-1$
        js.append("var windowHeight = document.body.clientHeight; "); //$NON-NLS-1$
      } else {
        js.append("var scrollPosY = window.pageYOffset; "); //$NON-NLS-1$
        js.append("var windowHeight = window.innerHeight; "); //$NON-NLS-1$
      }

      /* Find the last visible news of the group */
      long lastVisibleNewsId = -1;
      for (int i = newsIds.size() - 1; i >= 0; i--) {
        if (fViewModel.isNewsVisible(newsIds.get(i))) {
          lastVisibleNewsId = newsIds.get(i);
          break;
        }
      }

      if (lastVisibleNewsId != -1) {
        js.append("var divPosY = ").append(getElementById(Dynamic.NEWS.getId(lastVisibleNewsId))).append(".offsetTop; "); //$NON-NLS-1$ //$NON-NLS-2$
        js.append("var divHeight = ").append(getElementById(Dynamic.NEWS.getId(lastVisibleNewsId))).append(".offsetHeight; "); //$NON-NLS-1$ //$NON-NLS-2$
        js.append("if (scrollPosY + windowHeight < divPosY + divHeight) {"); //$NON-NLS-1$ //Scroll up to reveal as much news as possible from the group
        js.append(getElementById(Dynamic.TOGGLE_GROUP_IMG.getId(groupId))).append(".scrollIntoView(true); "); //$NON-NLS-1$
        js.append("}"); //$NON-NLS-1$
      }
    }

    /* Execute */
    fBrowser.execute(js.toString(), "setGroupExpanded"); //$NON-NLS-1$

    /* Update View Model */
    fViewModel.setGroupExpanded(groupId, expanded);
  }

  private void blur(String elementId) {
    blur(elementId, false);
  }

  private void blur(String elementId, boolean guardNull) {
    StringBuilder js = new StringBuilder();
    if (guardNull) {
      js.append("if (").append(getElementById(elementId)).append(") {"); //$NON-NLS-1$ //$NON-NLS-2$
      js.append(getElementById(elementId).append(".blur();")); //$NON-NLS-1$
      js.append("}"); //$NON-NLS-1$
    } else
      js.append(getElementById(elementId).append(".blur();")); //$NON-NLS-1$
    fBrowser.execute(js.toString(), "blur"); //$NON-NLS-1$
  }

  private void transformNews(final INews news) {
    String link = CoreUtils.getLink(news);
    if (!StringUtils.isSet(link))
      return;

    /* Indicate Progress */
    StringBuilder js = new StringBuilder();
    js.append(getElementById(Dynamic.FULL_CONTENT_LINK_TEXT.getId(news)).append(".innerHTML='").append(escapeForInnerHtml(Messages.NewsBrowserViewer_LOADING)).append("'; ")); //$NON-NLS-1$ //$NON-NLS-2$
    js.append(getElementById(Dynamic.FULL_CONTENT_LINK.getId(news)).append(".blur(); ")); //$NON-NLS-1$
    js.append(getElementById(Dynamic.FULL_CONTENT_LINK.getId(news)).append(".style.fontStyle = 'italic'; ")); //$NON-NLS-1$
    fBrowser.execute(js.toString(), "transformNews"); //$NON-NLS-1$

    /* First cancel all running jobs for this news if any */
    NewsReference reference = news.toReference();
    Job.getJobManager().cancel(reference);

    /* Load news content in background and update HTML afterwards */
    final String transformedUrl = Controller.getDefault().getEmbeddedTransformedUrl(link);
    UIBackgroundJob transformationJob = new UIBackgroundJob(fBrowser.getControl(), Messages.NewsBrowserViewer_RETRIEVING_ARTICLE_CONTENT, reference) {
      StringBuilder result = new StringBuilder();

      @Override
      protected void runInBackground(IProgressMonitor monitor) {
        try {
          URI uri = new URI(transformedUrl);
          IProtocolHandler handler = Owl.getConnectionService().getHandler(uri);
          if (handler != null) {
            BufferedReader reader = null;
            try {
              InputStream inS = handler.openStream(uri, monitor, null);
              reader = new BufferedReader(new InputStreamReader(inS, "UTF-8")); //$NON-NLS-1$
              String line;
              while (!monitor.isCanceled() && (line = reader.readLine()) != null) {
                result.append(line);
              }
            } catch (IOException e) {
              Activator.getDefault().logError(e.getMessage(), e);
              monitor.setCanceled(true);
            } finally {
              if (reader != null) {
                try {
                  reader.close();
                } catch (IOException e) {
                  monitor.setCanceled(true);
                }
              }
            }
          }
        } catch (URISyntaxException e) {
          Activator.getDefault().logError(e.getMessage(), e);
          monitor.setCanceled(true);
        } catch (ConnectionException e) {
          Activator.getDefault().logError(e.getMessage(), e);
          monitor.setCanceled(true);
        }
      }

      @Override
      protected void runInUI(IProgressMonitor monitor) {
        if (result.length() > 0 && !monitor.isCanceled() && !fBrowser.getControl().isDisposed())
          showTransformation(news, result.toString());
      }
    };

    JobRunner.runUIUpdater(transformationJob, true);
  }

  private void showTransformation(INews news, String result) {

    /* Transformer script surrounds content with a DIV, display it inline to avoid extra whitespace */
    int index = result.indexOf("<div"); //$NON-NLS-1$
    if (index != -1 && index < 10) { //Ensure the DIV is really at the beginning
      index += "<div".length(); //$NON-NLS-1$
      StringBuilder inlineResult = new StringBuilder();
      inlineResult.append(result.substring(0, index));
      inlineResult.append(" style=\"display: inline;\" "); //$NON-NLS-1$
      inlineResult.append(result.substring(index));
      result = inlineResult.toString();
    }

    /* Support stripping media tags if set and highlight search terms if any */
    IBaseLabelProvider labelProvider = getLabelProvider();
    if (labelProvider instanceof NewsBrowserLabelProvider) {
      result = ((NewsBrowserLabelProvider) labelProvider).stripMediaTagsIfNecessary(result);
      result = ((NewsBrowserLabelProvider) labelProvider).highlightSearchTermsIfNecessary(result);
    }

    final StringBuilder js = new StringBuilder();
    js.append(getElementById(Dynamic.CONTENT.getId(news)).append(".innerHTML='").append(escapeForInnerHtml(result)).append("'; ")); //$NON-NLS-1$ //$NON-NLS-2$
    js.append(getElementById(Dynamic.FULL_CONTENT_LINK.getId(news)).append(".style.fontStyle = 'normal'; ")); //$NON-NLS-1$
    js.append(getElementById(Dynamic.FULL_CONTENT_LINK_TEXT.getId(news)).append(".innerHTML='").append(escapeForInnerHtml(Messages.NewsBrowserViewer_FULL_CONTENT)).append("'; ")); //$NON-NLS-1$ //$NON-NLS-2$
    scrollIfNecessary(news, js);

    /* Block external navigation while setting innerHTML */
    fBrowser.blockExternalNavigationWhile(new Runnable() {
      public void run() {
        fBrowser.execute(js.toString(), "showTransformation"); //$NON-NLS-1$
      }
    });
  }

  private String escapeForInnerHtml(String str) {
    StringBuilder result = new StringBuilder(str.length());

    BufferedReader reader = new BufferedReader(new StringReader(str));
    String line;
    try {
      while ((line = reader.readLine()) != null) {

        /* Escape single and double quotes */
        line = StringUtils.replaceAll(line, "\"", "\\\""); //$NON-NLS-1$ //$NON-NLS-2$
        line = StringUtils.replaceAll(line, "'", "\\'"); //$NON-NLS-1$ //$NON-NLS-2$

        /* XULRunner: Need to escape % with its ASCII HEX counterpart as XUL decodes it */
        if (CBrowser.isMozillaRunningOnWindows())
          line = StringUtils.replaceAll(line, "%", "%25"); //$NON-NLS-1$ //$NON-NLS-2$

        /* Properly escape multilines */
        result.append(line).append("\\n"); //$NON-NLS-1$
      }
    } catch (IOException e) {
    }

    return result.toString().trim();
  }

  @SuppressWarnings("unchecked")
  private void revealNextPage(boolean scrollIntoView) {

    /* Get next page from View Model */
    Pair<List<Long>, List<Long>> nextPage = fViewModel.getNextPage(fPageSize);
    List<Long> revealedGroups = nextPage.getFirst();
    List<Long> revealedNews = nextPage.getSecond();

    revealItems(revealedGroups, revealedNews, scrollIntoView);
  }

  @SuppressWarnings("unchecked")
  private void revealItems(List<Long> revealedGroups, List<Long> revealedNews, boolean scrollIntoView) {

    /* Return early if no more news to reveal */
    if (revealedNews.isEmpty())
      return;

    final StringBuilder js = new StringBuilder();

    /* Blur Links */
    if (scrollIntoView) //Action was invoked from latch
      js.append(getElementById(Dynamic.PAGE_LATCH_LINK.getId()).append(".blur(); ")); //$NON-NLS-1$

    /* Check if the first revealed news belongs to a group that is already showing but collapsed */
    long group = fViewModel.findGroup(revealedNews.get(0));
    if (group != -1 && fViewModel.isGroupVisible(group) && !fViewModel.isGroupExpanded(group))
      setGroupExpanded(group, fViewModel.getNewsIds(group), true, false);

    /* Reveal Groups */
    for (Long groupId : revealedGroups) {
      if (fViewModel.isGroupVisible(groupId))
        continue; //Skip if already visible

      /* Update View Model */
      fViewModel.setGroupVisible(groupId, true);

      /* Show Group */
      js.append(getElementById(Dynamic.GROUP.getId(groupId))).append(".style.display='block'; "); //$NON-NLS-1$
    }

    /* Reveal News */
    Long firstNewsId = null;
    for (Long newsId : revealedNews) {
      if (fViewModel.isNewsVisible(newsId))
        continue; //Skip if already visible

      /* Remember the first news made visible to scroll to */
      if (firstNewsId == null)
        firstNewsId = newsId;

      /* Update View Model */
      fViewModel.setNewsVisible(newsId, true);

      /* Get News from Contentprovider */
      INews news = resolve(newsId);
      if (news == null)
        continue;

      final StringBuilder newsJs = new StringBuilder(); //Use own builder since News JS can be large depending on content

      String html = ((NewsBrowserLabelProvider) getLabelProvider()).getLabel(news, true, true, true, fViewModel.indexOfNewsItem(newsId));
      newsJs.append(getElementById(Dynamic.NEWS.getId(newsId))).append(".style.display='block'; "); //$NON-NLS-1$
      newsJs.append(getElementById(Dynamic.NEWS.getId(newsId))).append(".innerHTML ='").append(escapeForInnerHtml(html)).append("'; "); //$NON-NLS-1$ //$NON-NLS-2$;
      if (isHeadlinesLayout())
        newsJs.append(getElementById(Dynamic.HEADLINE_SEPARATOR.getId(newsId))).append(".style.display='block'; "); //$NON-NLS-1$

      /* Block external navigation while setting innerHTML */
      fBrowser.blockExternalNavigationWhile(new Runnable() {
        public void run() {
          fBrowser.execute(newsJs.toString(), "revealItems#0"); //$NON-NLS-1$
        }
      });
    }

    /* Update Scroll Position */
    if (firstNewsId != null && scrollIntoView)
      js.append(getElementById(Dynamic.NEWS.getId(firstNewsId))).append(".scrollIntoView(true); "); //$NON-NLS-1$

    /* Update Latch if necessary */
    updateLatchIfNecessary(js);

    /* Block external navigation while setting innerHTML */
    fBrowser.blockExternalNavigationWhile(new Runnable() {
      public void run() {
        fBrowser.execute(js.toString(), "revealItems#1"); //$NON-NLS-1$
      }
    });
  }

  private void updateLatchIfNecessary(StringBuilder js) {
    if (fPageSize == 0)
      return; //Return if pagination turned off

    /* Latch no longer required - hide */
    if (fViewModel.getVisibleNewsCount() == fViewModel.getNewsCount()) {
      js.append("var latch = ").append(getElementById(Dynamic.PAGE_LATCH.getId())).append("; "); //$NON-NLS-1$ //$NON-NLS-2$
      js.append("if (latch) {"); //$NON-NLS-1$
      js.append("  latch.style.display='none'; "); //$NON-NLS-1$
      js.append("}"); //$NON-NLS-1$
    }

    /* Otherwise update Latch Label */
    else if (getLabelProvider() instanceof NewsBrowserLabelProvider) {
      String updatedLatchName = ((NewsBrowserLabelProvider) getLabelProvider()).getLatchName();
      js.append("var latch = ").append(getElementById(Dynamic.PAGE_LATCH_TEXT.getId())).append("; "); //$NON-NLS-1$ //$NON-NLS-2$
      js.append("if (latch) {"); //$NON-NLS-1$
      js.append("  latch.innerHTML='").append(escapeForInnerHtml(updatedLatchName)).append("'; "); //$NON-NLS-1$ //$NON-NLS-2$
      js.append("}"); //$NON-NLS-1$
    }
  }

  private void delayInUI(Runnable runnable) {
    JobRunner.runInUIThread(0, true, getControl(), runnable);
  }

  private long getId(String query) {
    return Long.parseLong(query);
  }

  private INews getNews(String query) {
    try {
      long id = getId(query);
      return resolve(id);
    } catch (NullPointerException e) {
      return null;
    } catch (NumberFormatException e) {
      return null;
    }
  }

  private List<INews> getNewsList(String query) {
    List<INews> news = new ArrayList<INews>();

    StringTokenizer tokenizer = new StringTokenizer(query, ","); //$NON-NLS-1$
    while (tokenizer.hasMoreTokens()) {
      String nextElement = tokenizer.nextToken();
      if (StringUtils.isSet(nextElement)) {
        INews item = getNews(nextElement);
        if (item != null)
          news.add(item);
      }
    }

    return news;
  }

  private EntityGroup getEntityGroup(String query) {
    long id = getId(query);

    /* Try to resolve the news from the mapping table */
    List<Long> newsIds = fViewModel.getNewsIds(id);
    if (!newsIds.isEmpty()) {
      List<INews> news = new ArrayList<INews>(newsIds.size());
      for (Long newsId : newsIds) {
        try {
          INews item = resolve(newsId);
          if (item != null)
            news.add(item);
        } catch (NullPointerException e) {
          continue;
        }
      }

      /* Create a temporary new EntityGroup to be used from the context menu */
      EntityGroup group = new EntityGroup(id, NewsGrouping.GROUP_CATEGORY_ID);
      for (INews item : news) {
        new EntityGroupItem(group, item);
      }

      return group;
    }

    return null;
  }

  /*
   * @see org.eclipse.jface.viewers.ContentViewer#setLabelProvider(org.eclipse.jface.viewers.IBaseLabelProvider)
   */
  @Override
  public void setLabelProvider(IBaseLabelProvider labelProvider) {
    fBlockRefresh = true;
    try {
      super.setLabelProvider(labelProvider);
    } finally {
      fBlockRefresh = false;
    }
  }

  /*
   * @see org.eclipse.jface.viewers.ContentViewer#setContentProvider(org.eclipse.jface.viewers.IContentProvider)
   */
  @Override
  public void setContentProvider(IContentProvider contentProvider) {
    fBlockRefresh = true;
    try {
      super.setContentProvider(contentProvider);
    } finally {
      fBlockRefresh = false;
    }
  }

  /*
   * @see org.eclipse.jface.viewers.Viewer#refresh()
   */
  @Override
  public void refresh() {
    if (!fBlockRefresh) {
      fBrowser.refresh();
      onRefresh();
    }
  }

  /**
   * A special way of refreshing this viewer with additional options to control
   * the behavior.
   *
   * @param restoreInput if set to <code>true</code> will restore the initial
   * input that was set to the browser in case the user navigated to a different
   * URL.
   * @param moveToTop if <code>true</code> will scroll the browser to the top
   * position to reveal additional content.
   */
  public void refresh(boolean restoreInput, boolean moveToTop) {

    /* Browser not showing initial input anymore, so restore if asked for */
    if (restoreInput && !ApplicationServer.getDefault().isDisplayOperation(fBrowser.getControl().getUrl()))
      internalSetInput(fInput, true, false);

    /* Otherwise perform the normal refresh */
    else {

      /* Move scroll position to top if set */
      if (moveToTop)
        fBrowser.execute("scroll(0,0);", "refresh"); //$NON-NLS-1$ //$NON-NLS-2$

      /* Refresh */
      refresh();
    }
  }

  /**
   * Method is called whenever the viewer is refreshed. Subclasses my override
   * to do something.
   */
  protected void onRefresh() {
    //Do nothing here.
  }

  /*
   * @see org.eclipse.jface.viewers.ContentViewer#handleDispose(org.eclipse.swt.events.DisposeEvent)
   */
  @Override
  protected void handleDispose(DisposeEvent event) {
    fServer.unregister(fId);
    fCurrentSelection = null;
    OwlUI.safeDispose(fNewsContextMenu);
    OwlUI.safeDispose(fAttachmentsContextMenu);
    OwlUI.safeDispose(fLabelsContextMenu);
    OwlUI.safeDispose(fShareNewsContextMenu);
    OwlUI.safeDispose(fFindRelatedContextMenu);
    super.handleDispose(event);
  }

  /*
   * @see org.eclipse.jface.viewers.ContentViewer#setInput(java.lang.Object)
   */
  @Override
  public void setInput(Object input) {
    setInput(input, false);
  }

  /**
   * @param input the input to show in this news browser viewer.
   * @param blockExternalNavigation <code>true</code> to block any potential
   * external navigation when setting the input and <code>false</code> otherwise
   * (default).
   */
  public void setInput(Object input, boolean blockExternalNavigation) {
    internalSetInput(input, false, blockExternalNavigation);
  }

  /**
   * @param newLayout the new {@link Layout} to use in this viewer.
   */
  void onLayoutChanged(Layout newLayout) {
    if (fSite == null)
      return;

    /* Update Page Size */
    if (newLayout == Layout.HEADLINES || newLayout == Layout.NEWSPAPER)
      fPageSize = OwlUI.getPageSize(fSite.getInputPreferences()).getPageSize();
    else
      fPageSize = PageSize.NO_PAGING.getPageSize();

    /* Update "Mark Read on Scrolling" */
    fMarkReadOnScrolling = (newLayout == Layout.NEWSPAPER) && fSite.getInputPreferences().getBoolean(DefaultPreferences.MARK_READ_ON_SCROLLING);
  }

  private void internalSetInput(Object input, boolean force, boolean blockExternalNavigation) {

    /* Ignore this Input if its already set */
    if (!force && sameInput(input))
      return;

    /* Remember Input */
    fInput = input;

    /* Clear Cache of news marked as unread by user */
    fMarkedUnreadByUserCache.clear();

    /* Update Settings based on Input */
    if (fSite != null) {
      IPreferenceScope inputPreferences = fSite.getInputPreferences();
      fMarkReadOnExpand = inputPreferences.getBoolean(DefaultPreferences.MARK_READ_STATE);

      /* Update settings based on Layout */
      Layout layout = OwlUI.getLayout(inputPreferences);
      onLayoutChanged(layout);
    }

    /* Stop any other Website if required */
    String url = fBrowser.getControl().getUrl();
    if (!"".equals(url)) //$NON-NLS-1$
      fBrowser.getControl().stop();

    /* Input is a URL - display it */
    if (input instanceof String) {
      fBrowser.setUrl((String) input, !blockExternalNavigation);
      return;
    }

    /* Set URL if its not already showing and contains a display-operation */
    String inputUrl = fServer.toUrl(fId, input);
    if (fServer.isDisplayOperation(inputUrl) && !inputUrl.equals(url))
      fBrowser.setUrl(inputUrl);

    /* Hide the Browser as soon as the input is set to Null */
    if (input == null && fBrowser.getControl().getVisible())
      fBrowser.getControl().setVisible(false);
  }

  /* Checks wether the given Input is same to the existing one */
  private boolean sameInput(Object input) {
    if (fInput instanceof Object[])
      return input instanceof Object[] && Arrays.equals((Object[]) fInput, (Object[]) input);

    if (fInput != null)
      return fInput.equals(input);

    return false;
  }

  /*
   * @see org.eclipse.jface.viewers.ContentViewer#getInput()
   */
  @Override
  public Object getInput() {
    return fInput;
  }

  /**
   * Adds the given filter to this viewer.
   *
   * @param filter a viewer filter
   */
  public void addFilter(ViewerFilter filter) {
    if (fFilters == null)
      fFilters = new ArrayList<ViewerFilter>();

    fFilters.add(filter);
    if (filter instanceof NewsFilter)
      fNewsFilter = (NewsFilter) filter;
  }

  /**
   * Removes the given filter from this viewer, and triggers refiltering and
   * resorting of the elements if required. Has no effect if the identical
   * filter is not registered.
   *
   * @param filter a viewer filter
   */
  public void removeFilter(ViewerFilter filter) {
    Assert.isNotNull(filter);
    if (fFilters != null) {
      for (Iterator<ViewerFilter> i = fFilters.iterator(); i.hasNext();) {
        Object o = i.next();
        if (o == filter) {
          i.remove();
          refresh();
          if (fFilters.size() == 0)
            fFilters = null;

          return;
        }
      }
    }

    if (filter == fNewsFilter)
      fNewsFilter = null;
  }

  /**
   * @param comparator
   */
  public void setComparator(ViewerComparator comparator) {
    if (fSorter != comparator)
      fSorter = comparator;
  }

  /*
   * @see org.eclipse.jface.viewers.Viewer#getControl()
   */
  @Override
  public Control getControl() {
    return fBrowser.getControl();
  }

  /**
   * @return The wrapped Browser (CBrowser).
   */
  public CBrowser getBrowser() {
    return fBrowser;
  }

  /*
   * @see org.eclipse.jface.viewers.Viewer#getSelection()
   */
  @Override
  public ISelection getSelection() {
    return fCurrentSelection;
  }

  /*
   * @see org.eclipse.jface.viewers.Viewer#setSelection(org.eclipse.jface.viewers.ISelection,
   * boolean)
   */
  @Override
  public void setSelection(ISelection selection, boolean reveal) {
    fCurrentSelection = (IStructuredSelection) selection;
    fireSelectionChanged(new SelectionChangedEvent(this, selection));
  }

  /* Find a news from the selection and scroll it into view using JavaScript */
  void showSelection(ISelection selection) {
    if (!(selection instanceof StructuredSelection) || selection.isEmpty())
      return;

    /* Find the News to Show */
    NewsReference newsToShow = null;
    Object firstElement = ((StructuredSelection) selection).getFirstElement();
    if (firstElement instanceof INews)
      newsToShow = ((INews) firstElement).toReference();
    else if (firstElement instanceof NewsReference)
      newsToShow = (NewsReference) firstElement;

    /* Scroll the News into View if present and expand as necessary */
    if (newsToShow != null) {

      /* First determine if there are hidden elements that need to be expanded */
      Pair<List<Long>, List<Long>> itemsToReveal = fViewModel.revealPage(newsToShow.getId(), fPageSize);
      revealItems(itemsToReveal.getFirst(), itemsToReveal.getSecond(), false);

      /* Headlines Layout */
      if (isHeadlinesLayout()) {
        INews news = resolve(newsToShow.getId());
        if (news != null)
          setNewsExpanded(news, true);
      }

      /* Newspaper Layout */
      else {
        StringBuilder js = new StringBuilder();
        js.append(getElementById(Dynamic.NEWS.getId(newsToShow))).append(".scrollIntoView(true);"); //$NON-NLS-1$
        fBrowser.execute(js.toString(), "showSelection"); //$NON-NLS-1$
      }
    }
  }

  void navigate(boolean next, boolean unread, boolean onInputSet) {

    /* Navigate in Headlines Layout based on expanded element */
    if (isHeadlinesLayout())
      navigateInHeadlines(next, unread);

    /* Navigate in Newspaper Layout based on scroll position */
    else
      navigateInNewspaper(next, unread, onInputSet);
  }

  private void navigateInNewspaper(boolean next, boolean unread, boolean onInputSet) {

    /* Check if the first news is already unread and in this case avoid navigation */
    if (unread && onInputSet && fViewModel.isFirstItemUnread())
      return;

    /* Otherwise need to navigate to a specific unread news */
    StringBuilder js = new StringBuilder();
    if (fBrowser.isIE())
      js.append("var scrollPosY = document.body.scrollTop; "); //$NON-NLS-1$
    else
      js.append("var scrollPosY = window.pageYOffset; "); //$NON-NLS-1$
    js.append("var body = ").append(getElementById(BODY_ELEMENT_ID)).append("; "); //$NON-NLS-1$ //$NON-NLS-2$
    js.append("var divs = body.childNodes; "); //$NON-NLS-1$

    /* Next News (need to fake Y position by some pixels to avoid the same news being selected over and over) */
    if (next) {
      js.append("  for (var i = 1; i < divs.length; i++) { "); //$NON-NLS-1$
      js.append("    if (divs[i].nodeType != 1) { "); //$NON-NLS-1$
      js.append("      continue; "); //$NON-NLS-1$
      js.append("    } "); //$NON-NLS-1$
      js.append("    var divPosY = divs[i].offsetTop; "); //$NON-NLS-1$
      if (unread) {
        js.append("  if (divPosY > scrollPosY + 15 && divs[i].className == \"newsitemUnread\") { "); //$NON-NLS-1$
      } else
        js.append("  if (divPosY > scrollPosY + 15 && (divs[i].className == \"newsitemUnread\" || divs[i].className == \"newsitemRead\")) { "); //$NON-NLS-1$
      js.append("      divs[i].scrollIntoView(true); "); //$NON-NLS-1$
      js.append("      break; "); //$NON-NLS-1$
      js.append("    } "); //$NON-NLS-1$
      js.append("  } "); //$NON-NLS-1$
    }

    /* Previous News (need to fake Y position by some pixels to avoid the same news being selected over and over) */
    else {
      js.append("  for (var i = divs.length - 1; i >= 0; i--) { "); //$NON-NLS-1$
      js.append("    if (divs[i].nodeType != 1) { "); //$NON-NLS-1$
      js.append("      continue; "); //$NON-NLS-1$
      js.append("    } "); //$NON-NLS-1$
      js.append("    var divPosY = divs[i].offsetTop; "); //$NON-NLS-1$
      if (unread) {
        js.append("  if (divPosY < scrollPosY - 15 && divs[i].className == \"newsitemUnread\") { "); //$NON-NLS-1$
      } else
        js.append("  if (divPosY < scrollPosY - 15 && (divs[i].className == \"newsitemUnread\" || divs[i].className == \"newsitemRead\")) { "); //$NON-NLS-1$
      js.append("      divs[i].scrollIntoView(true); "); //$NON-NLS-1$
      js.append("      break; "); //$NON-NLS-1$
      js.append("    } "); //$NON-NLS-1$
      js.append("  } "); //$NON-NLS-1$
    }

    /* See if the Scroll Position Changed at all and handle */
    if (fBrowser.isIE())
      js.append("var newScrollPosY = document.body.scrollTop; "); //$NON-NLS-1$
    else
      js.append("var newScrollPosY = window.pageYOffset; "); //$NON-NLS-1$

    if (unread || fViewModel.hasItems()) { //Workaround for a Bug that would cause endless navigation if viewer contains no news otherwise
      js.append("if (scrollPosY == newScrollPosY) { "); //$NON-NLS-1$
      js.append("  window.location.href = '").append(ILinkHandler.HANDLER_PROTOCOL + getNavigationActionId(next, unread)).append("'; "); //$NON-NLS-1$ //$NON-NLS-2$
      js.append("} "); //$NON-NLS-1$
    }

    /* Execute in Browser */
    fBrowser.execute(js.toString(), "navigateInNewspaper"); //$NON-NLS-1$

    /* If we are at the end of the page, reveal more items if possible */
    onUserInteraction();
  }

  private void navigateInHeadlines(boolean next, boolean unread) {
    long targetNews = -1L;
    long offset = fViewModel.getExpandedNews();

    /* Navigate to Next News */
    if (next)
      targetNews = fViewModel.nextNews(unread, offset);

    /* Navigate to Previous News */
    else
      targetNews = fViewModel.previousNews(unread, offset);

    /* Expand target and the parent group if necessary */
    if (targetNews != -1) {

      /* First determine if there are hidden elements that need to be expanded */
      Pair<List<Long>, List<Long>> itemsToReveal = fViewModel.revealPage(targetNews, fPageSize);
      revealItems(itemsToReveal.getFirst(), itemsToReveal.getSecond(), false);

      /* Group */
      long groupId = fViewModel.findGroup(targetNews);
      if (groupId != -1 && !fViewModel.isGroupExpanded(groupId)) {
        List<Long> newsIds = fViewModel.getNewsIds(groupId);
        if (!newsIds.isEmpty())
          setGroupExpanded(groupId, newsIds, true, false);
      }

      /* News */
      setNewsExpanded(resolve(targetNews), true, false);
      StringBuilder js = new StringBuilder();
      js.append(getElementById(Dynamic.NEWS.getId(targetNews))).append(".scrollIntoView(true); "); //$NON-NLS-1$
      fBrowser.execute(js.toString(), "navigateInHeadlines#0"); //$NON-NLS-1$

      /* If we are at the end of the page, reveal more items if possible */
      onUserInteraction();
    }

    /* If navigation did not find a suitable target, call the outer navigation function */
    else if (unread || fViewModel.hasItems()) { //Workaround for a Bug that would cause endless navigation if viewer contains no news otherwise
      StringBuilder js = new StringBuilder();
      js.append("window.location.href = '").append(ILinkHandler.HANDLER_PROTOCOL + getNavigationActionId(next, unread)).append("'; "); //$NON-NLS-1$ //$NON-NLS-2$
      fBrowser.execute(js.toString(), "navigateInHeadlines#1"); //$NON-NLS-1$
    }
  }

  private String getNavigationActionId(boolean next, boolean unread) {
    if (next) {
      if (unread)
        return NewsBrowserViewer.NEXT_UNREAD_NEWS_HANDLER_ID;

      return NewsBrowserViewer.NEXT_NEWS_HANDLER_ID;
    }

    if (unread)
      return NewsBrowserViewer.PREVIOUS_UNREAD_NEWS_HANDLER_ID;

    return NewsBrowserViewer.PREVIOUS_NEWS_HANDLER_ID;
  }

  /**
   * Shows the intial Input in the Browser.
   */
  public void home() {
    internalSetInput(fInput, true, false);
  }

  /**
   * @return the {@link ViewerComparator} used for sorting news.
   */
  ViewerComparator getComparator() {
    return fSorter;
  }

  private Object[] getSortedChildren(Object parent) {
    Object[] result = getFilteredChildren(parent);
    if (fSorter != null) {

      /* Avoid modifying the original array from the model */
      result = result.clone();
      fSorter.sort(this, result);
    }

    return result;
  }

  private Object[] getFilteredChildren(Object parent) {
    Object[] result = getRawChildren(parent);

    /* Never filter a selected News, thereby return here */
    if (fInput instanceof INews)
      return result;

    /* Run Filters over result */
    if (fFilters != null) {
      for (Object filter : fFilters) {
        ViewerFilter f = (ViewerFilter) filter;
        result = f.filter(this, parent, result);
      }
    }
    return result;
  }

  private Object[] getRawChildren(Object parent) {
    Object[] result = null;
    if (parent != null) {
      IStructuredContentProvider cp = (IStructuredContentProvider) getContentProvider();
      if (cp != null)
        result = cp.getElements(parent);
    }
    return (result != null) ? result : new Object[0];
  }

  /**
   * Asks the NewsViewModel to update based on the given input.
   *
   * @param input the list of elements that becomes visible in the browser
   * viewer.
   */
  public void updateViewModel(Object[] input) {
    fViewModel.setInput(input, fPageSize);
  }

  /**
   * @param input Can either be an Array of Feeds or News
   * @return An flattend array of Objects.
   */
  public Object[] getFlattendChildren(Object input) {
    return getFlattendChildren(input, true);
  }

  /**
   * @param input Can either be an Array of Feeds or News
   * @param withGroups if <code>true</code> also return groups if present.
   * @return An flattend array of Objects.
   */
  public Object[] getFlattendChildren(Object input, boolean withGroups) {

    /* Using NewsContentProvider */
    if (input != null && getContentProvider() instanceof NewsContentProvider) {
      NewsContentProvider cp = (NewsContentProvider) getContentProvider();

      /*
       * Flatten Children since Grouping is Enabled and the Parent is not
       * containing just News (so either Feed or ViewerGroups).
       */
      if (cp.isGroupingEnabled() && !isNews(input)) {
        List<Object> flatList = new ArrayList<Object>();

        /* Wrap into Object-Array */
        if (!(input instanceof Object[]))
          input = new Object[] { input };

        /* For each Group retrieve Children (sorted and filtered) */
        int newsCount = 0;
        Object groups[] = (Object[]) input;
        for (Object group : groups) {

          /* Make sure this child has children */
          if (cp.hasChildren(group)) {
            Object sortedChilds[] = getSortedChildren(group);

            /* Only add if there are Childs */
            if (sortedChilds.length > 0) {

              /* Add the Group itself */
              if (withGroups)
                flatList.add(group);

              /* Store the actual number of news of the group too */
              if (group instanceof EntityGroup)
                ((EntityGroup) group).setSizeHint(sortedChilds.length);

              /* Add childs of group to the list */
              flatList.addAll(Arrays.asList(sortedChilds));
              newsCount += sortedChilds.length;
            }
          }

          /* Otherwise just add */
          else {
            if (withGroups)
              flatList.add(group);
          }
        }

        return fillPagingIfNecessary(flatList.toArray(), newsCount);
      }

      /* Grouping is not enabled, just return sorted Children */
      return fillPagingIfNecessary(getSortedChildren(input));
    }

    /* Structured ContentProvider */
    else if (input != null && getContentProvider() instanceof IStructuredContentProvider)
      return getSortedChildren(input);

    /* No Element to show */
    return new Object[0];
  }

  private Object[] fillPagingIfNecessary(Object[] elements) {
    return fillPagingIfNecessary(elements, elements.length);
  }

  private Object[] fillPagingIfNecessary(Object[] elements, int newsCount) {
    if (fPageSize == 0 || newsCount <= fPageSize)
      return elements;

    Object[] elementsWithPaging = new Object[elements.length + 1];
    System.arraycopy(elements, 0, elementsWithPaging, 0, elements.length);
    elementsWithPaging[elements.length] = new PageLatch();

    return elementsWithPaging;
  }

  /* Returns TRUE if the Input consists only of INews */
  private boolean isNews(Object input) {
    if (input instanceof Object[]) {
      Object elements[] = (Object[]) input;
      for (Object element : elements) {
        if (!(element instanceof INews))
          return false;
      }
    } else if (!(input instanceof INews))
      return false;

    return true;
  }

  /**
   * @param parentElement
   * @param childElements
   */
  public void add(Object parentElement, Object[] childElements) {
    Assert.isNotNull(parentElement);
    assertElementsNotNull(childElements);

    if (childElements.length > 0)
      refresh(); // TODO Optimize
  }

  /**
   * @param news
   */
  public void update(Collection<NewsEvent> news) {

    /*
     * The update-event could have been sent out a lot faster than the Browser
     * having a chance to react. In this case, rather then refreshing a possible
     * blank page (or wrong page), re-set the input.
     */
    String inputUrl = fServer.toUrl(fId, fInput);
    String browserUrl = fBrowser.getControl().getUrl();
    boolean resetInput = browserUrl.length() == 0 || URIUtils.ABOUT_BLANK.equals(browserUrl);
    if (inputUrl.equals(browserUrl)) {
      if (!internalUpdate(news))
        refresh(); // Refresh if dynamic update failed
    } else if (fServer.isDisplayOperation(inputUrl) && resetInput)
      fBrowser.setUrl(inputUrl);
  }

  /**
   * @param objects
   */
  public void remove(Object[] objects) {
    assertElementsNotNull(objects);

    /* Refresh if dynamic removal failed */
    if (!internalRemove(objects))
      refresh();
  }

  /**
   * @param element
   */
  public void remove(Object element) {
    Assert.isNotNull(element);

    /* Refresh if dynamic removal failed */
    if (!internalRemove(new Object[] { element }))
      refresh();
  }

  private boolean internalUpdate(Collection<NewsEvent> newsEvents) {
    boolean toggleJS = fBrowser.shouldDisableScript();
    try {
      if (toggleJS)
        fBrowser.setScriptDisabled(false);

      /* Update for each Event */
      for (NewsEvent newsEvent : newsEvents) {
        INews news = newsEvent.getEntity();
        if (!fViewModel.isNewsVisible(news) || news.getId() == null || !fViewModel.hasNews(news.getId()))
          continue; //Do not update if news not visible at all or not part of the browsers content

        StringBuilder js = new StringBuilder();

        /* State (Bold/Plain Title, Mark Read Tooltip) */
        if (CoreUtils.isStateChange(newsEvent)) {
          String markRead = Messages.NewsBrowserViewer_MARK_READ;
          String markUnread = Messages.NewsBrowserViewer_MARK_UNREAD;

          boolean isRead = (INews.State.READ == news.getState());
          js.append(getElementById(Dynamic.NEWS.getId(news)).append(isRead ? ".className='newsitemRead'; " : ".className='newsitemUnread'; ")); //$NON-NLS-1$ //$NON-NLS-2$
          js.append(getElementById(Dynamic.TITLE_LINK.getId(news)).append(isRead ? ".className='read'; " : ".className='unread'; ")); //$NON-NLS-1$ //$NON-NLS-2$
          js.append(getElementById(Dynamic.TOGGLE_READ_LINK.getId(news)).append(isRead ? ".title='" + markUnread + "'; " : ".title='" + markRead + "'; ")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
          js.append(getElementById(Dynamic.TOGGLE_READ_IMG.getId(news)).append(isRead ? ".alt='" + markUnread + "'; " : ".alt='" + markRead + "'; ")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
        }

        /* Sticky (Title Background, Footer Background, Mark Sticky Image) */
        if (CoreUtils.isStickyStateChange(newsEvent)) {
          boolean isSticky = news.isFlagged();
          js.append(getElementById(Dynamic.HEADER.getId(news)).append(isSticky ? ".className='headerSticky'; " : ".className='header'; ")); //$NON-NLS-1$ //$NON-NLS-2$

          js.append("var footer = ").append(getElementById(Dynamic.FOOTER.getId(news))).append("; "); //$NON-NLS-1$ //$NON-NLS-2$
          js.append("if (footer) {"); //$NON-NLS-1$
          js.append("  footer").append(isSticky ? ".className='footerSticky'; " : ".className='footer'; "); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
          js.append("}"); //$NON-NLS-1$

          String stickyImgUri;
          if (fBrowser.isIE())
            stickyImgUri = isSticky ? OwlUI.getImageUri("/icons/obj16/news_pinned_light.gif", "news_pinned_light.gif") : OwlUI.getImageUri("/icons/obj16/news_pin_light.gif", "news_pin_light.gif"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
          else
            stickyImgUri = isSticky ? ApplicationServer.getDefault().toResourceUrl("/icons/obj16/news_pinned_light.gif") : ApplicationServer.getDefault().toResourceUrl("/icons/obj16/news_pin_light.gif"); //$NON-NLS-1$ //$NON-NLS-2$

          js.append(getElementById(Dynamic.TOGGLE_STICKY_IMG.getId(news)).append(".src='").append(stickyImgUri).append("'; ")); //$NON-NLS-1$ //$NON-NLS-2$
        }

        /* Label (Title Foreground, Label List) */
        if (CoreUtils.isLabelChange(newsEvent)) {
          Set<ILabel> labels = CoreUtils.getSortedLabels(news);
          String defaultColor = (CoreUtils.getLink(news) != null && !isHeadlinesLayout()) ? "#009" : "rgb(0,0,0)"; //$NON-NLS-1$ //$NON-NLS-2$
          String color = (labels.isEmpty()) ? defaultColor : "rgb(" + OwlUI.toString(OwlUI.getRGB(labels.iterator().next())) + ")"; //$NON-NLS-1$ //$NON-NLS-2$
          if ("rgb(0,0,0)".equals(color)) //Don't let black override link color //$NON-NLS-1$
            color = defaultColor;
          js.append(getElementById(Dynamic.TITLE_LINK.getId(news)).append(".style.color='").append(color).append("'; ")); //$NON-NLS-1$ //$NON-NLS-2$

          /* Remove Labels */
          if (labels.isEmpty()) {
            js.append(getElementById(Dynamic.LABELS_SEPARATOR.getId(news)).append(".style.display='none'; ")); //$NON-NLS-1$
            js.append(getElementById(Dynamic.LABELS.getId(news)).append(".innerHTML=''; ")); //$NON-NLS-1$
          }

          /* Show Labels */
          else {
            js.append(getElementById(Dynamic.LABELS_SEPARATOR.getId(news)).append(".style.display='inline'; ")); //$NON-NLS-1$

            StringBuilder labelsHtml = new StringBuilder(Messages.NewsBrowserViewer_LABELS);
            labelsHtml.append(" "); //$NON-NLS-1$
            int c = 0;
            for (ILabel label : labels) {
              c++;
              if (c < labels.size())
                span(labelsHtml, StringUtils.htmlEscape(label.getName()) + ", ", label.getColor()); //$NON-NLS-1$
              else
                span(labelsHtml, StringUtils.htmlEscape(label.getName()), label.getColor());
            }

            js.append(getElementById(Dynamic.LABELS.getId(news)).append(".innerHTML='").append(escapeForInnerHtml(labelsHtml.toString())).append("'; ")); //$NON-NLS-1$ //$NON-NLS-2$
          }

          /* Make sure to also update collapsed subtitles if present */
          if (isHeadlinesLayout() && !fViewModel.isNewsExpanded(news)) {
            StringBuilder subtitleContent = new StringBuilder();
            IBaseLabelProvider lp = getLabelProvider();
            if (lp instanceof NewsBrowserLabelProvider)
              ((NewsBrowserLabelProvider) lp).fillSubtitle(subtitleContent, news, labels, false);
            if (subtitleContent.length() > 0)
              js.append(getElementById(Dynamic.SUBTITLE_LINK.getId(news))).append(".innerHTML='").append(escapeForInnerHtml(subtitleContent.toString())).append("'; "); //$NON-NLS-1$ //$NON-NLS-2$
          }
        }

        /* Execute */
        if (js.length() > 0) {
          boolean res = fBrowser.execute(js.toString(), false, "internalUpdate"); //$NON-NLS-1$
          if (!res)
            return false;
        }
      }
    } finally {
      if (toggleJS)
        fBrowser.setScriptDisabled(true);
    }

    return true;
  }

  private void span(StringBuilder builder, String content, String color) {
    builder.append("<span style=\"color: rgb(").append(color).append(");\""); //$NON-NLS-1$ //$NON-NLS-2$
    builder.append(">").append(content).append("</span>"); //$NON-NLS-1$ //$NON-NLS-2$
  }

  private StringBuilder getElementById(String id) {
    return new StringBuilder("document.getElementById('" + id + "')"); //$NON-NLS-1$ //$NON-NLS-2$
  }

  private boolean internalRemove(Object[] elements) {
    StringBuilder js = new StringBuilder();
    js.append("var body = ").append(getElementById(BODY_ELEMENT_ID)).append("; "); //$NON-NLS-1$ //$NON-NLS-2$

    boolean varDefined = false;
    Set<Long> groupsToUpdate = new HashSet<Long>();
    for (Object element : elements) {
      if (element instanceof INews) {
        INews news = (INews) element;
        if (news.getId() == null || !fViewModel.hasNews(news.getId()))
          continue; //Do not update if news not part of the browsers content at all

        /* Remove from View Model */
        long groupToUpdate = fViewModel.removeNews(news);
        if (groupToUpdate != -1)
          groupsToUpdate.add(groupToUpdate);

        /* Remove News from DOM */
        if (!varDefined) {
          js.append("var "); //$NON-NLS-1$
          varDefined = true;
        }

        js.append("node = ").append(getElementById(Dynamic.NEWS.getId(news))).append("; "); //$NON-NLS-1$ //$NON-NLS-2$
        js.append("if (node && node.parentNode == body) { "); //$NON-NLS-1$
        js.append("  body.removeChild(node); "); //$NON-NLS-1$
        js.append("} else if (node) { "); //$NON-NLS-1$
        js.append("  node.className='hidden';"); //$NON-NLS-1$
        js.append("} "); //$NON-NLS-1$

        /* Hide Separator if using headlines layout */
        if (isHeadlinesLayout()) {
          js.append("node = ").append(getElementById(Dynamic.HEADLINE_SEPARATOR.getId(news))).append("; "); //$NON-NLS-1$ //$NON-NLS-2$
          js.append("if (node && node.parentNode == body) { "); //$NON-NLS-1$
          js.append("  body.removeChild(node); "); //$NON-NLS-1$
          js.append("} else if (node) {"); //$NON-NLS-1$
          js.append("  node.className='hidden';"); //$NON-NLS-1$
          js.append("} "); //$NON-NLS-1$
        }
      }
    }

    /* Update Groups */
    IBaseLabelProvider labelProvider = getLabelProvider();
    for (Long groupId : groupsToUpdate) {

      /* Remove Groups from DOM or update it */
      if (!varDefined) {
        js.append("var "); //$NON-NLS-1$
        varDefined = true;
      }

      /* Group is empty now: Remove it from DOM */
      if (!fViewModel.hasGroup(groupId)) {
        js.append("node = ").append(getElementById(Dynamic.GROUP.getId(groupId))).append("; "); //$NON-NLS-1$ //$NON-NLS-2$
        js.append("if (node && node.parentNode == body) { "); //$NON-NLS-1$
        js.append("  body.removeChild(node); "); //$NON-NLS-1$
        js.append("} else if (node) { "); //$NON-NLS-1$
        js.append("  node.className='hidden';"); //$NON-NLS-1$
        js.append("} "); //$NON-NLS-1$
      }

      /* Group has a new Element count: Update it */
      else if (labelProvider instanceof NewsBrowserLabelProvider) {
        int count = fViewModel.getGroupSize(groupId);
        NewsBrowserLabelProvider browserLabelProvider = (NewsBrowserLabelProvider) labelProvider;
        String groupNote = browserLabelProvider.getGroupNote(count, count);

        js.append("node = ").append(getElementById(Dynamic.GROUP_NOTE.getId(groupId))).append("; "); //$NON-NLS-1$ //$NON-NLS-2$
        js.append("if (node) { "); //$NON-NLS-1$
        js.append("  node.innerHTML = '").append(escapeForInnerHtml(groupNote)).append("'; "); //$NON-NLS-1$ //$NON-NLS-2$
        js.append("} "); //$NON-NLS-1$
      }
    }

    /* Update Latch as necessary */
    updateLatchIfNecessary(js);

    return fBrowser.execute(js.toString(), "internalRemove"); //$NON-NLS-1$
  }

  private void assertElementsNotNull(Object[] elements) {
    Assert.isNotNull(elements);
    for (Object element : elements) {
      Assert.isNotNull(element);
    }
  }

  /**
   * @return Returns a List of Strings that should get highlighted per News that
   * is displayed.
   */
  protected Collection<String> getHighlightedWords() {
    if (getContentProvider() instanceof NewsContentProvider && fPreferences.getBoolean(DefaultPreferences.FV_HIGHLIGHT_SEARCH_RESULTS)) {
      INewsMark mark = ((NewsContentProvider) getContentProvider()).getInput();
      Set<String> extractedWords;

      /* Extract from Conditions if any */
      if (mark instanceof ISearch) {
        List<ISearchCondition> conditions = ((ISearch) mark).getSearchConditions();
        extractedWords = CoreUtils.extractWords(conditions);
      } else
        extractedWords = new HashSet<String>(1);

      /* Fill Pattern if set */
      if (fNewsFilter != null && StringUtils.isSet(fNewsFilter.getPatternString())) {
        String pattern = fNewsFilter.getPatternString();

        /* News Filter sometimes converts to wildcard query */
        if (StringUtils.supportsTrailingWildcards(pattern))
          pattern = pattern + "*"; //$NON-NLS-1$

        /* Extract Words */
        extractedWords.addAll(CoreUtils.extractWords(pattern));
      }

      return extractedWords;
    }

    return Collections.emptyList();
  }

  NewsBrowserViewModel getViewModel() {
    return fViewModel;
  }

  private boolean isGroupingEnabled() {
    IContentProvider cp = getContentProvider();
    if (cp instanceof NewsContentProvider)
      return ((NewsContentProvider) cp).isGroupingEnabled();

    return false;
  }

  private boolean isGroupingByState() {
    IContentProvider cp = getContentProvider();
    if (cp instanceof NewsContentProvider)
      return ((NewsContentProvider) cp).isGroupingByState();

    return false;
  }

  private boolean isHeadlinesLayout() {
    IBaseLabelProvider lp = getLabelProvider();
    if (lp instanceof NewsBrowserLabelProvider)
      return ((NewsBrowserLabelProvider) lp).isHeadlinesOnly();

    return false;
  }

  INews resolve(long newsId) {
    INews news = null;

    /* First Ask ContentProvider Cache */
    if (getContentProvider() instanceof NewsContentProvider)
      news = ((NewsContentProvider) getContentProvider()).obtainFromCache(newsId);

    /* Then fallback to resolve from DB */
    if (news == null)
      return fNewsDao.load(newsId);

    return news;
  }
}
TOP

Related Classes of org.rssowl.ui.internal.editors.feed.NewsBrowserViewer

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.