org.rssowl.ui.internal.editors.feed.FeedView.java Source code

Java tutorial

Introduction

Here is the source code for org.rssowl.ui.internal.editors.feed.FeedView.java

Source

/*   **********************************************************************  **
 **   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.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.IAction;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.LocalResourceManager;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IPartListener2;
import org.eclipse.ui.IReusableEditor;
import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.part.EditorPart;
import org.rssowl.core.Owl;
import org.rssowl.core.connection.ConnectionException;
import org.rssowl.core.connection.IAbortable;
import org.rssowl.core.connection.IProtocolHandler;
import org.rssowl.core.internal.persist.pref.DefaultPreferences;
import org.rssowl.core.persist.IBookMark;
import org.rssowl.core.persist.IEntity;
import org.rssowl.core.persist.IFeed;
import org.rssowl.core.persist.IFolder;
import org.rssowl.core.persist.IMark;
import org.rssowl.core.persist.INews;
import org.rssowl.core.persist.INewsBin;
import org.rssowl.core.persist.INewsMark;
import org.rssowl.core.persist.ISearchCondition;
import org.rssowl.core.persist.ISearchMark;
import org.rssowl.core.persist.dao.DynamicDAO;
import org.rssowl.core.persist.dao.IBookMarkDAO;
import org.rssowl.core.persist.dao.INewsBinDAO;
import org.rssowl.core.persist.dao.INewsDAO;
import org.rssowl.core.persist.dao.ISearchMarkDAO;
import org.rssowl.core.persist.event.BookMarkAdapter;
import org.rssowl.core.persist.event.BookMarkEvent;
import org.rssowl.core.persist.event.BookMarkListener;
import org.rssowl.core.persist.event.FeedAdapter;
import org.rssowl.core.persist.event.FeedEvent;
import org.rssowl.core.persist.event.FolderAdapter;
import org.rssowl.core.persist.event.FolderEvent;
import org.rssowl.core.persist.event.MarkEvent;
import org.rssowl.core.persist.event.NewsBinAdapter;
import org.rssowl.core.persist.event.NewsBinEvent;
import org.rssowl.core.persist.event.NewsBinListener;
import org.rssowl.core.persist.event.SearchConditionEvent;
import org.rssowl.core.persist.event.SearchConditionListener;
import org.rssowl.core.persist.event.SearchMarkAdapter;
import org.rssowl.core.persist.event.SearchMarkEvent;
import org.rssowl.core.persist.pref.IPreferenceScope;
import org.rssowl.core.persist.reference.FeedLinkReference;
import org.rssowl.core.persist.reference.NewsReference;
import org.rssowl.core.util.CoreUtils;
import org.rssowl.core.util.ITreeNode;
import org.rssowl.core.util.LoggingSafeRunnable;
import org.rssowl.core.util.RetentionStrategy;
import org.rssowl.core.util.StringUtils;
import org.rssowl.core.util.TreeTraversal;
import org.rssowl.core.util.URIUtils;
import org.rssowl.ui.internal.Activator;
import org.rssowl.ui.internal.Application;
import org.rssowl.ui.internal.ApplicationServer;
import org.rssowl.ui.internal.Controller;
import org.rssowl.ui.internal.Controller.BookMarkLoadListener;
import org.rssowl.ui.internal.FolderNewsMark;
import org.rssowl.ui.internal.OwlUI;
import org.rssowl.ui.internal.OwlUI.Layout;
import org.rssowl.ui.internal.actions.DeleteTypesAction;
import org.rssowl.ui.internal.actions.FindAction;
import org.rssowl.ui.internal.actions.ReloadTypesAction;
import org.rssowl.ui.internal.actions.RetargetActions;
import org.rssowl.ui.internal.undo.NewsStateOperation;
import org.rssowl.ui.internal.undo.UndoStack;
import org.rssowl.ui.internal.util.CBrowser;
import org.rssowl.ui.internal.util.EditorUtils;
import org.rssowl.ui.internal.util.JobRunner;
import org.rssowl.ui.internal.util.LayoutUtils;
import org.rssowl.ui.internal.util.UIBackgroundJob;
import org.rssowl.ui.internal.util.WidgetTreeNode;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

/**
 * The FeedView is an instance of <code>EditorPart</code> capable of displaying
 * News in a Table-Viewer and Browser-Viewer. It offers controls to Filter and
 * Group them.
 *
 * @author bpasero
 */
public class FeedView extends EditorPart implements IReusableEditor {

    /* Delay in millies to Mark *new* News to *unread* on Part-Deactivation */
    private static final int HANDLE_NEWS_SEEN_DELAY = 100;

    /* Millies before news seen are handled */
    private static final int HANDLE_NEWS_SEEN_BLOCK_DELAY = 800;

    /* Delay in millies to safely operate on the browser content */
    private static final int BROWSER_OPERATIONS_DELAY = 100;

    /* Millies before the next clean up is allowed to run again */
    private static final int CLEAN_UP_BLOCK_DELAY = 1000;

    /* System Property indicating the separator to use for CSV files */
    private static final String CSV_SEPARATOR_PROPERTY = "csvSeparator"; //$NON-NLS-1$

    /* The last visible Feedview */
    private static FeedView fgLastVisibleFeedView = null;

    /* Flag to indicate if feed change events should be blocked or not */
    private static boolean fgBlockFeedChangeEvent;

    /** ID of this EditorPart */
    public static final String ID = "org.rssowl.ui.FeedView"; //$NON-NLS-1$

    /** List of UI-Events interesting for the FeedView */
    public enum UIEvent {

        /** Other Feed Displayed */
        FEED_CHANGE,

        /** Application Minimized */
        MINIMIZE,

        /** Application Closing */
        CLOSE,

        /** Tab Closed */
        TAB_CLOSE
    }

    /* Editor Data */
    private FeedViewInput fInput;
    private IEditorSite fEditorSite;
    private IFeedViewSite fFeedViewSite;

    /* Part to display News in Table */
    private NewsTableControl fNewsTableControl;

    /* Part to display News in Browser */
    private NewsBrowserControl fNewsBrowserControl;

    /* Bars */
    private FilterBar fFilterBar;
    private BrowserBar fBrowserBar;

    /* Shared Viewer classes */
    private NewsFilter fNewsFilter;
    private NewsGrouping fNewsGrouping;
    private NewsContentProvider fContentProvider;

    /* Container for the News Table Viewer */
    private Composite fNewsTableControlContainer;

    /* Container for the Browser Viewer */
    private Composite fBrowserViewerControlContainer;

    /* Listeners */
    private IPartListener2 fPartListener;
    private BookMarkListener fBookMarkListener;
    private SearchMarkAdapter fSearchMarkListener;
    private FeedAdapter fFeedListener;
    private SearchConditionListener fSearchConditionListener;
    private NewsBinListener fNewsBinListener;
    private FolderAdapter fFolderListener;
    private BookMarkLoadListener fBookMarkLoadListener;

    /* Settings */
    NewsFilter.Type fInitialFilterType;
    NewsGrouping.Type fInitialGroupType;
    NewsFilter.SearchTarget fInitialSearchTarget;
    Layout fLayout;
    private int fInitialWeights[];
    private int fCacheWeights[];

    /* Global Actions */
    private IAction fReloadAction;
    private IAction fSelectAllAction;
    private IAction fDeleteAction;
    private IAction fCutAction;
    private IAction fCopyAction;
    private IAction fPasteAction;
    private IAction fPrintAction;
    private IAction fUndoAction;
    private IAction fRedoAction;
    private IAction fFindAction;

    /* Misc. */
    private Composite fParent;
    private Composite fRootComposite;
    private SashForm fSashForm;
    private Label fHorizontalTableBrowserSep;
    private Label fVerticalTableBrowserSep;
    private LocalResourceManager fResourceManager;
    private IPreferenceScope fPreferences;
    private long fOpenTime;
    private boolean fCreated;
    private final Object fCacheJobIdentifier = new Object();
    private ImageDescriptor fTitleImageDescriptor;
    private Label fHorizontalFilterTableSep;
    private Label fHorizontalBrowserSep;
    private Label fVerticalBrowserSep;
    private final INewsDAO fNewsDao = Owl.getPersistenceService().getDAOService().getNewsDAO();
    private boolean fIsDisposed;
    private AtomicLong fLastCleanUpRun = new AtomicLong();

    /*
     * @see org.eclipse.ui.part.EditorPart#doSave(org.eclipse.core.runtime.IProgressMonitor)
     */
    @Override
    public void doSave(IProgressMonitor monitor) {
        /* Not Supported */
    }

    /*
     * @see org.eclipse.ui.part.EditorPart#doSaveAs()
     */
    @Override
    public void doSaveAs() {
        if (fIsDisposed || Controller.getDefault().isShuttingDown())
            return;

        /* Ask user for File */
        FileDialog dialog = new FileDialog(getSite().getShell(), SWT.SAVE);
        dialog.setOverwrite(true);

        List<String> extensions = new ArrayList<String>();
        extensions.add("*.html"); //$NON-NLS-1$

        if (fInput.getMark() instanceof IBookMark)
            extensions.add("*.xml"); //$NON-NLS-1$

        if (isTableViewerVisible())
            extensions.add("*.csv"); //$NON-NLS-1$

        dialog.setFilterExtensions(extensions.toArray(new String[extensions.size()]));

        String proposedName = Application.IS_WINDOWS ? CoreUtils.getSafeFileNameForWindows(fInput.getName())
                : fInput.getName();
        proposedName += ".html"; //$NON-NLS-1$
        dialog.setFileName(proposedName);

        String fileName = dialog.open();
        if (fileName == null)
            return;

        if (fileName.endsWith(".xml")) //$NON-NLS-1$
            saveAsXml(fileName);
        else if (fileName.endsWith(".csv")) //$NON-NLS-1$
            saveAsCsv(fileName);
        else
            saveAsHtml(fileName);
    }

    @SuppressWarnings("restriction")
    private void saveAsXml(final String fileName) {
        final IBookMark bm = (IBookMark) fInput.getMark();
        final URI feedLink = bm.getFeedLinkReference().getLink();
        try {
            final IProtocolHandler handler = Owl.getConnectionService().getHandler(feedLink);
            if (handler instanceof org.rssowl.core.internal.connection.DefaultProtocolHandler) {
                Job downloadJob = new Job(Messages.FeedView_DOWNLOADING_FEED) {
                    @Override
                    protected IStatus run(IProgressMonitor monitor) {
                        monitor.beginTask(bm.getName(), IProgressMonitor.UNKNOWN);

                        InputStream in = null;
                        FileOutputStream out = null;
                        boolean canceled = false;
                        Exception error = null;
                        try {
                            byte[] buffer = new byte[8192];

                            in = handler.openStream(feedLink, monitor, null);
                            out = new FileOutputStream(fileName);
                            while (true) {

                                /* Check for Cancellation and Shutdown */
                                if (monitor.isCanceled() || Controller.getDefault().isShuttingDown()) {
                                    canceled = true;
                                    return Status.CANCEL_STATUS;
                                }

                                /* Read from Stream */
                                int read = in.read(buffer);
                                if (read == -1)
                                    break;

                                out.write(buffer, 0, read);
                            }
                        } catch (FileNotFoundException e) {
                            error = e;
                            Activator.safeLogError(e.getMessage(), e);
                        } catch (IOException e) {
                            error = e;
                            Activator.safeLogError(e.getMessage(), e);
                        } catch (ConnectionException e) {
                            error = e;
                            Activator.safeLogError(e.getMessage(), e);
                        } finally {
                            monitor.done();

                            if (out != null) {
                                try {
                                    out.close();
                                } catch (IOException e) {
                                    Activator.safeLogError(e.getMessage(), e);
                                }
                            }

                            if (in != null) {
                                try {
                                    if ((canceled || error != null) && in instanceof IAbortable)
                                        ((IAbortable) in).abort();
                                    else
                                        in.close();
                                } catch (IOException e) {
                                    Activator.safeLogError(e.getMessage(), e);
                                }
                            }
                        }

                        return Status.OK_STATUS;
                    }
                };
                downloadJob.schedule();
            }
        } catch (ConnectionException e) {
            Activator.safeLogError(e.getMessage(), e);
        }
    }

    /* Build Content as String from Feed */
    private void saveAsHtml(String fileName) {
        StringBuilder content = new StringBuilder();
        NewsBrowserLabelProvider labelProvider = (NewsBrowserLabelProvider) fNewsBrowserControl.getViewer()
                .getLabelProvider();

        URI base = null;
        if (fInput.getMark() instanceof IBookMark) {
            try {
                base = URIUtils
                        .toHTTP(new URI(((IBookMark) fInput.getMark()).getFeedLinkReference().getLinkAsText()));
            } catch (URISyntaxException e) {
                /* Ignore and fallback to not using a Base at all */
            }
        }

        /* Save from Table */
        if (isTableViewerVisible()) {
            Tree tree = fNewsTableControl.getViewer().getTree();
            TreeItem[] items = tree.getItems();
            if (items.length > 0) {
                List<INews> newsToSave = new ArrayList<INews>();

                /* Ungrouped */
                if (items[0].getItemCount() == 0) {
                    for (TreeItem item : items) {
                        if (item.getData() instanceof INews)
                            newsToSave.add((INews) item.getData());
                    }
                }

                /* Grouped */
                else {
                    for (TreeItem parentItem : items) {
                        TreeItem[] childItems = parentItem.getItems();
                        for (TreeItem item : childItems) {
                            if (item.getData() instanceof INews)
                                newsToSave.add((INews) item.getData());
                        }
                    }
                }

                /* Render Elements */
                String text = labelProvider.render(newsToSave.toArray(), base, false);
                content.append(text);
            }
        }

        /* Save from Browser */
        else {
            NewsBrowserViewer viewer = fNewsBrowserControl.getViewer();
            Object[] elements = fContentProvider.getElements(fInput.getMark().toReference());
            elements = viewer.getFlattendChildren(elements, false);

            /* Render Elements */
            String text = labelProvider.render(elements, base, false);
            content.append(text);
        }

        /* Write into File */
        if (content.length() > 0)
            CoreUtils.write(fileName, content);
    }

    private void saveAsCsv(final String fileName) {
        StringBuilder content = new StringBuilder();

        String separator = System.getProperty(CSV_SEPARATOR_PROPERTY);
        if (separator == null || separator.length() == 0)
            separator = ";"; //$NON-NLS-1$
        else if (separator.equals("\\t")) //$NON-NLS-1$
            separator = "\t"; //$NON-NLS-1$

        Tree tree = fNewsTableControl.getViewer().getTree();
        TreeItem[] items = tree.getItems();
        if (items.length > 0) {
            List<TreeItem> itemsToSave = new ArrayList<TreeItem>();

            /* Ungrouped */
            if (items[0].getItemCount() == 0) {
                for (TreeItem item : items) {
                    if (item.getData() instanceof INews)
                        itemsToSave.add(item);
                }
            }

            /* Grouped */
            else {
                for (TreeItem parentItem : items) {
                    TreeItem[] childItems = parentItem.getItems();
                    for (TreeItem item : childItems) {
                        if (item.getData() instanceof INews)
                            itemsToSave.add(item);
                    }
                }
            }

            /* Get header */
            for (int order : tree.getColumnOrder()) {
                TreeColumn column = tree.getColumn(order);
                String text = column.getText();
                if (text.length() > 0)
                    content.append(toCSVEntry(text, separator)).append(separator);
            }

            if (content.length() > 0) {
                content.delete(content.length() - separator.length(), content.length());
                content.append('\n');
            }

            /* Get contents */
            for (TreeItem item : itemsToSave) {
                boolean lineAdded = false;
                for (int order : tree.getColumnOrder()) {
                    if (tree.getColumn(order).getText().length() > 0) {
                        String text = item.getText(order);
                        content.append(toCSVEntry(text, separator)).append(separator);
                        lineAdded = true;
                    }
                }

                if (lineAdded) {
                    content.delete(content.length() - separator.length(), content.length());
                    content.append('\n');
                }
            }
        }

        /* Write into File */
        if (content.length() > 0)
            CoreUtils.write(fileName, content);
    }

    private String toCSVEntry(String value, String separator) {
        if (value.contains(separator)) {

            /* Values that contain the separator and quotes need to escape quotes */
            if (value.contains("\"")) { //$NON-NLS-1$
                value = StringUtils.replaceAll(value, "\"", "\"\""); //$NON-NLS-1$ //$NON-NLS-2$
            }

            /* Values that contain the separator needs to be surrounded by quotes */
            return "\"" + value + "\""; //$NON-NLS-1$ //$NON-NLS-2$
        }

        return value;
    }

    /*
     * @see org.eclipse.ui.part.EditorPart#init(org.eclipse.ui.IEditorSite,
     * org.eclipse.ui.IEditorInput)
     */
    @Override
    public void init(IEditorSite site, IEditorInput input) {
        Assert.isTrue(input instanceof FeedViewInput);

        fEditorSite = site;
        fFeedViewSite = new FeedViewSite(this, site);
        setSite(site);
        fResourceManager = new LocalResourceManager(JFaceResources.getResources());

        /* Load Settings */
        fPreferences = Owl.getPreferenceService().getGlobalScope();
        loadSettings((FeedViewInput) input);

        /* Apply Input */
        setInput(input);

        /* Hook into Global Actions */
        createGlobalActions();
        setGlobalActions();

        /* Register Listeners */
        registerListeners();
    }

    private boolean justOpened() {
        return System.currentTimeMillis() - fOpenTime < HANDLE_NEWS_SEEN_BLOCK_DELAY;
    }

    private void registerListeners() {
        fPartListener = new IPartListener2() {

            /* Mark *new* News as *unread* or *read* */
            public void partHidden(IWorkbenchPartReference partRef) {

                /* Return early if event is too close after opening the feed */
                if (justOpened())
                    return;

                /* Remember this feedview as being the last visible one */
                if (FeedView.this.equals(partRef.getPart(false)))
                    fgLastVisibleFeedView = FeedView.this;
            }

            /* Hook into Global Actions for this Editor */
            public void partBroughtToTop(IWorkbenchPartReference partRef) {
                if (FeedView.this.equals(partRef.getPart(false))) {
                    setGlobalActions();
                    OwlUI.updateWindowTitle(fInput != null ? fInput.getMark() : null);

                    /* Notify last visible feedview about change */
                    if (fgLastVisibleFeedView != null && fgLastVisibleFeedView != FeedView.this
                            && !fgLastVisibleFeedView.fIsDisposed) {
                        fgLastVisibleFeedView.notifyUIEvent(UIEvent.FEED_CHANGE);
                        fgLastVisibleFeedView = null;
                    }
                }

                /* Any other editor was brought to top, reset last visible feedview */
                else if (!ID.equals(partRef.getId()))
                    fgLastVisibleFeedView = null;
            }

            public void partClosed(IWorkbenchPartReference partRef) {
                IEditorReference[] editors = partRef.getPage().getEditorReferences();
                boolean equalsThis = FeedView.this.equals(partRef.getPart(false));
                if (editors.length == 0 && equalsThis)
                    OwlUI.updateWindowTitle((String) null);

                if (equalsThis) {
                    if (fgLastVisibleFeedView == FeedView.this) //Avoids duplicate UI Event handling
                        fgLastVisibleFeedView = null;
                    notifyUIEvent(UIEvent.TAB_CLOSE);
                }
            }

            public void partDeactivated(IWorkbenchPartReference partRef) {
            }

            public void partActivated(IWorkbenchPartReference partRef) {
                if (FeedView.this.equals(partRef.getPart(false)))
                    OwlUI.updateWindowTitle(fInput != null ? fInput.getMark() : null);
            }

            public void partInputChanged(IWorkbenchPartReference partRef) {
                if (FeedView.this.equals(partRef.getPart(false)))
                    OwlUI.updateWindowTitle(fInput != null ? fInput.getMark() : null);
            }

            public void partOpened(IWorkbenchPartReference partRef) {
                if (FeedView.this.equals(partRef.getPart(false)) && isVisible()) {
                    fOpenTime = System.currentTimeMillis();
                    OwlUI.updateWindowTitle(fInput != null ? fInput.getMark() : null);
                }
            }

            public void partVisible(IWorkbenchPartReference partRef) {
                if (FeedView.this.equals(partRef.getPart(false)))
                    OwlUI.updateWindowTitle(fInput != null ? fInput.getMark() : null);
            }
        };

        fEditorSite.getPage().addPartListener(fPartListener);

        /* React on Bookmark Events */
        fBookMarkListener = new BookMarkAdapter() {
            @Override
            public void entitiesDeleted(Set<BookMarkEvent> events) {
                onNewsMarksDeleted(events);
            }

            @Override
            public void entitiesUpdated(Set<BookMarkEvent> events) {
                onNewsMarksUpdated(events);
            }
        };
        DynamicDAO.addEntityListener(IBookMark.class, fBookMarkListener);

        /* React on Folder Events */
        fFolderListener = new FolderAdapter() {
            @Override
            public void entitiesDeleted(Set<FolderEvent> events) {
                onFoldersDeleted(events);
            }

            @Override
            public void entitiesUpdated(Set<FolderEvent> events) {
                onNewsFoldersUpdated(events);
            }
        };
        DynamicDAO.addEntityListener(IFolder.class, fFolderListener);

        /* React on Searchmark Events */
        fSearchMarkListener = new SearchMarkAdapter() {
            @Override
            public void entitiesDeleted(Set<SearchMarkEvent> events) {
                onNewsMarksDeleted(events);
            }

            @Override
            public void entitiesUpdated(Set<SearchMarkEvent> events) {
                onNewsMarksUpdated(events);
            }
        };
        DynamicDAO.addEntityListener(ISearchMark.class, fSearchMarkListener);

        /* Refresh on Condition Changes if SearchMark showing */
        fSearchConditionListener = new SearchConditionListener() {
            public void entitiesAdded(Set<SearchConditionEvent> events) {
                refreshIfRequired(events);
            }

            public void entitiesDeleted(Set<SearchConditionEvent> events) {
                /* Ignore Due to Bug 1140 (http://dev.rssowl.org/show_bug.cgi?id=1140) */
            }

            public void entitiesUpdated(Set<SearchConditionEvent> events) {
                /* Ignore Due to Bug 1140 (http://dev.rssowl.org/show_bug.cgi?id=1140) */
            }

            /* We rely on the implementation detail that updating a SM means deleting/adding conditions */
            private void refreshIfRequired(Set<SearchConditionEvent> events) {
                if (fInput.getMark() instanceof ISearchMark) {
                    ISearchMarkDAO dao = DynamicDAO.getDAO(ISearchMarkDAO.class);
                    for (SearchConditionEvent event : events) {
                        ISearchCondition condition = event.getEntity();
                        ISearchMark searchMark = dao.load(condition);
                        if (searchMark != null && searchMark.equals(fInput.getMark())) {
                            JobRunner.runUIUpdater(new UIBackgroundJob(fParent) {
                                @Override
                                protected void runInBackground(IProgressMonitor monitor) {
                                    if (!Controller.getDefault().isShuttingDown())
                                        fContentProvider.refreshCache(monitor, fInput.getMark());
                                }

                                @Override
                                protected void runInUI(IProgressMonitor monitor) {
                                    if (!Controller.getDefault().isShuttingDown())
                                        refresh(true, true);
                                }

                                @Override
                                public boolean belongsTo(Object family) {
                                    return fCacheJobIdentifier.equals(family);
                                }
                            });

                            break;
                        }
                    }
                }
            }
        };
        DynamicDAO.addEntityListener(ISearchCondition.class, fSearchConditionListener);

        /* React on Newsbin Events */
        fNewsBinListener = new NewsBinAdapter() {
            @Override
            public void entitiesDeleted(Set<NewsBinEvent> events) {
                onNewsMarksDeleted(events);
            }

            @Override
            public void entitiesUpdated(Set<NewsBinEvent> events) {
                onNewsMarksUpdated(events);
            }
        };
        DynamicDAO.addEntityListener(INewsBin.class, fNewsBinListener);

        /* Listen if Title Image is changing */
        fFeedListener = new FeedAdapter() {
            @Override
            public void entitiesUpdated(Set<FeedEvent> events) {

                /* Only supported for BookMarks */
                if (!(fInput.getMark() instanceof IBookMark) || events.size() == 0)
                    return;

                /* Check if Feed-Event affecting us */
                for (FeedEvent event : events) {
                    FeedLinkReference feedRef = ((IBookMark) fInput.getMark()).getFeedLinkReference();
                    if (feedRef.references(event.getEntity())) {
                        ImageDescriptor imageDesc = fInput.getImageDescriptor();

                        /* Title Image Change - Update! */
                        if (!fTitleImageDescriptor.equals(imageDesc)) {
                            fTitleImageDescriptor = imageDesc;

                            JobRunner.runInUIThread(fParent, new Runnable() {
                                public void run() {
                                    setTitleImage(OwlUI.getImage(fResourceManager, fTitleImageDescriptor));
                                }
                            });
                        }

                        break;
                    }
                }
            }
        };
        DynamicDAO.addEntityListener(IFeed.class, fFeedListener);

        /* Show Busy when Input is loaded */
        fBookMarkLoadListener = new Controller.BookMarkLoadListener() {
            public void bookMarkAboutToLoad(IBookMark bookmark) {
                if (!fIsDisposed && bookmark.equals(fInput.getMark()))
                    showBusyLoading(true);
            }

            public void bookMarkDoneLoading(IBookMark bookmark) {
                if (!fIsDisposed && bookmark.equals(fInput.getMark()))
                    showBusyLoading(false);
            }
        };
        Controller.getDefault().addBookMarkLoadListener(fBookMarkLoadListener);
    }

    private void showBusyLoading(final boolean busy) {
        JobRunner.runInUIThread(fParent, new Runnable() {
            @SuppressWarnings("restriction")
            public void run() {
                if (!fIsDisposed && getSite() instanceof org.eclipse.ui.internal.PartSite)
                    ((org.eclipse.ui.internal.PartSite) getSite()).getPane().setBusy(busy);
            }
        });
    }

    private void onNewsFoldersUpdated(final Set<FolderEvent> events) {
        JobRunner.runInUIThread(fParent, new Runnable() {
            public void run() {
                if (!(fInput.getMark() instanceof FolderNewsMark))
                    return;

                final IEditorPart activeFeedView = fEditorSite.getPage().getActiveEditor();
                FolderNewsMark folderNewsMark = (FolderNewsMark) (fInput.getMark());
                for (FolderEvent event : events) {
                    final IFolder folder = event.getEntity();
                    if (folder.equals(folderNewsMark.getFolder())) {
                        setPartName(folder.getName());
                        if (activeFeedView == FeedView.this)
                            OwlUI.updateWindowTitle(fInput.getMark());

                        break;
                    }
                }
            }
        });
    }

    private void onFoldersDeleted(Set<FolderEvent> events) {
        if (!(fInput.getMark() instanceof FolderNewsMark))
            return;

        FolderNewsMark folderNewsMark = (FolderNewsMark) (fInput.getMark());
        for (FolderEvent event : events) {
            final IFolder folder = event.getEntity();
            if (folder.equals(folderNewsMark.getFolder())) {
                fInput.setDeleted();
                JobRunner.runInUIThread(fParent, new Runnable() {
                    public void run() {
                        fEditorSite.getPage().closeEditor(FeedView.this, false);
                    }
                });
                break;
            }
        }
    }

    private void onNewsMarksUpdated(final Set<? extends MarkEvent> events) {
        JobRunner.runInUIThread(fParent, new Runnable() {
            public void run() {
                final IEditorPart activeFeedView = fEditorSite.getPage().getActiveEditor();
                for (MarkEvent event : events) {
                    final IMark mark = event.getEntity();
                    if (mark.getId().equals(fInput.getMark().getId())) {
                        setPartName(mark.getName());
                        if (activeFeedView == FeedView.this)
                            OwlUI.updateWindowTitle(fInput.getMark());

                        break;
                    }
                }
            }
        });
    }

    private void onNewsMarksDeleted(Set<? extends MarkEvent> events) {
        for (MarkEvent event : events) {
            IMark mark = event.getEntity();
            if (fInput.getMark().getId().equals(mark.getId())) {
                fInput.setDeleted();
                JobRunner.runInUIThread(fParent, new Runnable() {
                    public void run() {
                        fEditorSite.getPage().closeEditor(FeedView.this, false);
                    }
                });
                break;
            }
        }
    }

    private void loadSettings(FeedViewInput input) {

        /* Filter Settings */
        IPreferenceScope preferences = Owl.getPreferenceService().getEntityScope(input.getMark());
        int iVal = preferences.getInteger(DefaultPreferences.BM_NEWS_FILTERING);
        if (iVal >= 0)
            fInitialFilterType = NewsFilter.Type.values()[iVal];
        else
            fInitialFilterType = NewsFilter.Type.values()[fPreferences
                    .getInteger(DefaultPreferences.FV_FILTER_TYPE)];

        /* Group Settings */
        iVal = preferences.getInteger(DefaultPreferences.BM_NEWS_GROUPING);
        if (iVal >= 0)
            fInitialGroupType = NewsGrouping.Type.values()[iVal];
        else
            fInitialGroupType = NewsGrouping.Type.values()[fPreferences
                    .getInteger(DefaultPreferences.FV_GROUP_TYPE)];

        /* Other Settings */
        fLayout = OwlUI.getLayout(preferences);
        fInitialWeights = fPreferences.getIntegers(DefaultPreferences.FV_SASHFORM_WEIGHTS);
        fInitialSearchTarget = NewsFilter.SearchTarget.values()[fPreferences
                .getInteger(DefaultPreferences.FV_SEARCH_TARGET)];
    }

    private void saveSettings() {

        /* Update Settings in DB */
        if (fCacheWeights != null && fCacheWeights[0] != fCacheWeights[1]) {
            int weightDiff = (fInitialWeights[0] - fCacheWeights[0]);
            if (Math.abs(weightDiff) > 5) {
                int strWeights[] = new int[] { fCacheWeights[0], fCacheWeights[1] };
                fPreferences.putIntegers(DefaultPreferences.FV_SASHFORM_WEIGHTS, strWeights);
            }
        }
    }

    private void createGlobalActions() {

        /* Hook into Reload */
        fReloadAction = new Action() {
            @Override
            public void run() {
                new ReloadTypesAction(new StructuredSelection(fInput.getMark()), getEditorSite().getShell()).run();
            }
        };

        /* Select All */
        fSelectAllAction = new Action() {
            @Override
            public void run() {
                Control focusControl = fEditorSite.getShell().getDisplay().getFocusControl();

                /* Select All in Text Widget */
                if (focusControl instanceof Text) {
                    ((Text) focusControl).selectAll();
                }

                /* Select All in Viewer Tree */
                else {
                    ((Tree) fNewsTableControl.getViewer().getControl()).selectAll();
                    fNewsTableControl.getViewer().setSelection(fNewsTableControl.getViewer().getSelection());
                }
            }
        };

        /* Delete */
        fDeleteAction = new Action() {
            @Override
            public void run() {
                new DeleteTypesAction(fParent.getShell(),
                        (IStructuredSelection) fNewsTableControl.getViewer().getSelection()).run();
            }
        };

        /* Cut */
        fCutAction = new Action() {
            @Override
            public void run() {
                Control focusControl = fEditorSite.getShell().getDisplay().getFocusControl();

                /* Cut in Text Widget */
                if (focusControl instanceof Text)
                    ((Text) focusControl).cut();
            }
        };

        /* Copy */
        fCopyAction = new Action() {
            @Override
            public void run() {
                Control focusControl = fEditorSite.getShell().getDisplay().getFocusControl();

                /* Copy in Text Widget */
                if (focusControl instanceof Text)
                    ((Text) focusControl).copy();
            }
        };

        /* Paste */
        fPasteAction = new Action() {
            @Override
            public void run() {
                Control focusControl = fEditorSite.getShell().getDisplay().getFocusControl();

                /* Paste in Text Widget */
                if (focusControl instanceof Text)
                    ((Text) focusControl).paste();
            }
        };

        /* Print */
        fPrintAction = new Action() {
            @Override
            public void run() {
                print();
            }
        };

        /* Undo (Eclipse Integration) */
        fUndoAction = new Action() {
            @Override
            public void run() {
                UndoStack.getInstance().undo();
            }
        };

        /* Redo (Eclipse Integration) */
        fRedoAction = new Action() {
            @Override
            public void run() {
                UndoStack.getInstance().redo();
            }
        };

        /* Find (Eclipse Integration) */
        fFindAction = new FindAction();
    }

    /**
     * Print the contents of the Browser if any.
     */
    public void print() {

        /* Return early if the browser is not visible at all */
        if (!isBrowserViewerVisible()) {
            MessageDialog.openInformation(fRootComposite.getShell(), Messages.FeedView_PRINT_NEWS,
                    Messages.FeedView_PRINT_NEWS_HEADLINES_LAYOUT);
            return;
        }

        /* Pass request to browser */
        if (fNewsBrowserControl != null)
            fNewsBrowserControl.getViewer().getBrowser().print();
    }

    /**
     * The user performed the "Find" action.
     */
    public void find() {
        if (fFilterBar != null) {

            /* Make Feed Toolbar Visible if not visible yet */
            if (!fFilterBar.isVisible()) {
                fPreferences.putBoolean(DefaultPreferences.FV_FEED_TOOLBAR_HIDDEN, false);
                EditorUtils.updateToolbarVisibility();
            }

            fFilterBar.focusQuickSearch();
        }
    }

    private void setGlobalActions() {

        /* Define Retargetable Global Actions */
        fEditorSite.getActionBars().setGlobalActionHandler(RetargetActions.RELOAD, fReloadAction);
        fEditorSite.getActionBars().setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), fSelectAllAction);
        fEditorSite.getActionBars().setGlobalActionHandler(ActionFactory.DELETE.getId(), fDeleteAction);
        fEditorSite.getActionBars().setGlobalActionHandler(ActionFactory.CUT.getId(), fCutAction);
        fEditorSite.getActionBars().setGlobalActionHandler(ActionFactory.COPY.getId(), fCopyAction);
        fEditorSite.getActionBars().setGlobalActionHandler(ActionFactory.PASTE.getId(), fPasteAction);
        fEditorSite.getActionBars().setGlobalActionHandler(ActionFactory.PRINT.getId(), fPrintAction);
        fEditorSite.getActionBars().setGlobalActionHandler(ActionFactory.UNDO.getId(), fUndoAction);
        fEditorSite.getActionBars().setGlobalActionHandler(ActionFactory.REDO.getId(), fRedoAction);
        fEditorSite.getActionBars().setGlobalActionHandler(ActionFactory.FIND.getId(), fFindAction);

        /* Disable some Edit-Actions at first */
        fEditorSite.getActionBars().getGlobalActionHandler(ActionFactory.CUT.getId()).setEnabled(false);
        fEditorSite.getActionBars().getGlobalActionHandler(ActionFactory.COPY.getId()).setEnabled(false);
        fEditorSite.getActionBars().getGlobalActionHandler(ActionFactory.PASTE.getId()).setEnabled(false);
    }

    /**
     * Sets the given <code>IStructuredSelection</code> to the News-Table showing
     * in the FeedView. Will ignore the selection, if the Table is minimized.
     *
     * @param selection The Selection to show in the News-Table.
     */
    public void setSelection(final IStructuredSelection selection) {

        /* Remove Filter if selection is hidden */
        final AtomicBoolean unfilter = new AtomicBoolean(false);
        if (fNewsFilter.getType() != NewsFilter.Type.SHOW_ALL) {
            List<?> elements = selection.toList();
            for (Object element : elements) {

                /* Resolve the actual News */
                if (element instanceof NewsReference) {
                    INews news = fContentProvider.obtainFromCache(((NewsReference) element).getId());
                    if (news != null)
                        element = news;
                    else
                        element = ((NewsReference) element).resolve();
                }

                /* This Element is filtered */
                if (!fNewsFilter.select(fNewsTableControl.getViewer(), null, element)) {
                    unfilter.set(true);
                    break;
                }
            }
        }

        /* Remove Filter if selection is hidden */
        if (unfilter.get()) {

            /* Provide code to be executed after unfiltering is done */
            Runnable joinUIRunnable = new Runnable() {
                public void run() {
                    internalShowSelection(selection, unfilter);
                }
            };

            /* Remove Filter */
            fFilterBar.doFilter(NewsFilter.Type.SHOW_ALL, true, false, joinUIRunnable);
        }

        /* Directly show selection as filtering was not undone */
        else
            internalShowSelection(selection, unfilter);
    }

    private void internalShowSelection(final IStructuredSelection selection, final AtomicBoolean unfilter) {

        /* Scroll News into View from Browser if maximized */
        if (!isTableViewerVisible()) {
            JobRunner.runInUIThread(BROWSER_OPERATIONS_DELAY, fNewsBrowserControl.getViewer().getControl(),
                    new Runnable() {
                        public void run() { //Run delayed as the browser might still be busy loading the input
                            Runnable runnable = new Runnable() {
                                public void run() {
                                    fNewsBrowserControl.getViewer().showSelection(selection);
                                }
                            };

                            /* If Elements got revealed, make sure they show in the Browser viewer and then select the news item delayed */
                            if (unfilter.get()) {
                                fNewsBrowserControl.getViewer().refresh();
                                JobRunner.runInUIThread(BROWSER_OPERATIONS_DELAY,
                                        fNewsBrowserControl.getViewer().getControl(), runnable);
                            }

                            /* Otherwise directly select the news item */
                            else {
                                runnable.run();
                            }
                        }
                    });
        }

        /* Apply selection to Table */
        else
            fNewsTableControl.getViewer().setSelection(selection, true);
    }

    /*
     * @see org.eclipse.ui.part.EditorPart#setInput(org.eclipse.ui.IEditorInput)
     */
    @Override
    public void setInput(IEditorInput input) {
        Assert.isTrue(input instanceof FeedViewInput);

        /* Quickly cancel any caching Job and dispose content provider since input changed */
        if (fCreated) {

            /* Keep the news for passing into notifyUIEvent() */
            Collection<INews> cachedNewsCopy = fContentProvider.getCachedNewsCopy();

            /* Cancel and Dispose */
            Job.getJobManager().cancel(fCacheJobIdentifier);
            fContentProvider.dispose();

            /* Handle Old being hidden now */
            if (fInput != null) {
                notifyUIEvent(UIEvent.FEED_CHANGE, cachedNewsCopy);
                rememberSelection(fInput.getMark(), fNewsTableControl.getLastSelection());
            }
        }

        /* Set New */
        super.setInput(input);
        fInput = (FeedViewInput) input;

        /* Update UI of Feed-View if new Editor */
        if (!fCreated)
            updateTab(fInput);

        /* Clear Filter Bar */
        if (fFilterBar != null)
            fFilterBar.clearQuickSearch(false);

        /* Editor is being reused */
        if (fCreated) {
            firePropertyChange(PROP_INPUT);

            /* Load Filter Settings for this Mark if present */
            updateFilterAndGrouping(false);

            /* Re-Create the ContentProvider to avoid being blocked on the old content provider still resolving something */
            fContentProvider = new NewsContentProvider(fNewsTableControl.getViewer(),
                    fNewsBrowserControl.getViewer(), this);
            fNewsTableControl.getViewer().setContentProvider(fContentProvider);
            fNewsTableControl.onInputChanged(fInput);
            fNewsBrowserControl.getViewer().setContentProvider(fContentProvider);
            fNewsBrowserControl.onInputChanged(fInput);

            /* Reset the Quicksearch if active */
            if (fNewsFilter.isPatternSet())
                fNewsFilter.setPattern(""); //$NON-NLS-1$

            /* Update news mark in filter */
            fNewsFilter.setNewsMark(fInput.getMark());

            /* Apply Input */
            setInput(fInput.getMark(), true);
        }
    }

    /* Update Title and Image of the FeedView's Tab */
    private void updateTab(FeedViewInput input) {
        setPartName(input.getName());
        fTitleImageDescriptor = input.getImageDescriptor();
        setTitleImage(OwlUI.getImage(fResourceManager, fTitleImageDescriptor));
    }

    /**
     * Load Filter Settings for the Mark that is set as input if present
     * <p>
     * TODO Find a better solution once its possible to add listeners to
     * {@link IPreferenceScope} and then listen to changes of display-properties.
     * </p>
     *
     * @param refresh If TRUE, refresh the Viewer, FALSE otherwise.
     */
    public void updateFilterAndGrouping(boolean refresh) {
        IPreferenceScope preferences = Owl.getPreferenceService().getEntityScope(fInput.getMark());
        int iVal = preferences.getInteger(DefaultPreferences.BM_NEWS_FILTERING);
        if (iVal >= 0)
            fFilterBar.doFilter(NewsFilter.Type.values()[iVal], refresh, false);
        else
            fFilterBar.doFilter(
                    NewsFilter.Type.values()[fPreferences.getInteger(DefaultPreferences.FV_FILTER_TYPE)], refresh,
                    false);

        /* Load Group Settings for this Mark if present */
        iVal = preferences.getInteger(DefaultPreferences.BM_NEWS_GROUPING);
        if (iVal >= 0)
            fFilterBar.doGrouping(NewsGrouping.Type.values()[iVal], refresh, false);
        else
            fFilterBar.doGrouping(
                    NewsGrouping.Type.values()[fPreferences.getInteger(DefaultPreferences.FV_GROUP_TYPE)], refresh,
                    false);
    }

    /**
     * Refresh the visible columns of the opened news table control.
     */
    public void updateColumns() {
        if (fInput == null)
            return;

        /* Folder News Mark might require cache refresh if sorting has changed and limit reached */
        if (fInput.getMark() instanceof FolderNewsMark && fInput.getMark()
                .getNewsCount(INews.State.getVisible()) > NewsContentProvider.MAX_FOLDER_ELEMENTS) {
            FolderNewsMark folderMark = (FolderNewsMark) fInput.getMark();
            IPreferenceScope preferences = Owl.getPreferenceService().getEntityScope(folderMark.getFolder());
            NewsComparator comparator = getComparator();

            NewsColumn oldSortBy = comparator.getSortBy();
            boolean oldIsAscending = comparator.isAscending();

            NewsColumn newSortBy = NewsColumn.values()[preferences
                    .getInteger(DefaultPreferences.BM_NEWS_SORT_COLUMN)];
            boolean newIsAscending = preferences.getBoolean(DefaultPreferences.BM_NEWS_SORT_ASCENDING);

            /* Sorting changed and cache is at limit, so refresh cache */
            if (oldSortBy != newSortBy || oldIsAscending != newIsAscending) {
                NewsComparator comparer = new NewsComparator();
                comparer.setSortBy(newSortBy);
                comparer.setAscending(newIsAscending);

                fContentProvider.refreshCache(null, folderMark, comparer);
            }
        }

        /* Update Columns and Sorting in Table Viewer */
        if (isTableViewerVisible())
            fNewsTableControl.updateColumns(fInput.getMark());

        /* Update Sorting in Browser Viewer */
        if (isBrowserViewerVisible())
            fNewsBrowserControl.updateSorting(fInput.getMark(), true);
    }

    /**
     * Notifies this editor about a UI-Event just occured. In dependance of the
     * event, the Editor might want to update the state on the displayed News.
     *
     * @param event The UI-Event that just occured as described in the
     * <code>UIEvent</code> enumeration.
     */
    public void notifyUIEvent(final UIEvent event) {
        notifyUIEvent(event, null);
    }

    private void notifyUIEvent(final UIEvent event, Collection<INews> visibleNews) {
        final IMark inputMark = fInput.getMark();
        final IStructuredSelection lastSelection = fNewsTableControl.getLastSelection();

        /* Avoid any work in case RSSOwl is shutting down in an emergency */
        if (Controller.getDefault().isEmergencyShutdown())
            return;

        /* Specially Treat Restart Situation */
        if (Controller.getDefault().isRestarting()) {
            if (event == UIEvent.TAB_CLOSE && fInput.exists())
                rememberSelection(inputMark, lastSelection);

            return; // Ignore other events during restart
        }

        /* Specially Treat Closing Situation */
        else if (Controller.getDefault().isShuttingDown()) {
            if (event == UIEvent.TAB_CLOSE && fInput.exists())
                rememberSelection(inputMark, lastSelection);

            if (event != UIEvent.CLOSE)
                return; // Ignore other events than CLOSE that might get issued
        }

        /* Operate on a Copy of the Content Providers News (either passed in or obtain) */
        final Collection<INews> news = (visibleNews != null) ? filterHidden(visibleNews)
                : filterHidden(fContentProvider.getCachedNewsCopy());
        IPreferenceScope inputPreferences = Owl.getPreferenceService().getEntityScope(inputMark);

        /*
         * News can be NULL at this moment, if the Job that is to refresh the cache
         * in the Content Provider was never scheduled. This can happen when quickly
         * navigating between feeds. Also, the input could have been deleted and the
         * editor closed. Thereby do not react.
         */
        if (news.isEmpty() || !fInput.exists())
            return;

        final boolean markReadOnFeedChange = inputPreferences.getBoolean(DefaultPreferences.MARK_READ_ON_CHANGE);
        final boolean markReadOnTabClose = inputPreferences.getBoolean(DefaultPreferences.MARK_READ_ON_TAB_CLOSE);
        final boolean markReadOnMinimize = inputPreferences.getBoolean(DefaultPreferences.MARK_READ_ON_MINIMIZE);

        /* Mark *new* News as *unread* when closing the entire application */
        if (event == UIEvent.CLOSE) {

            /* Perform the State Change */
            List<INews> newsToUpdate = new ArrayList<INews>();
            for (INews newsItem : news) {
                if (newsItem.getState() == INews.State.NEW)
                    newsToUpdate.add(newsItem);
            }

            /* Perform Operation */
            fNewsDao.setState(newsToUpdate, INews.State.UNREAD, OwlUI.markReadDuplicates(), false);
        }

        /* Handle seen News: Feed Change (also closing the feed view), Closing or Minimize Event */
        else if (event == UIEvent.FEED_CHANGE || event == UIEvent.MINIMIZE || event == UIEvent.TAB_CLOSE) {

            /* Return early if this is a feed change which should be ignored */
            if (event == UIEvent.FEED_CHANGE && fgBlockFeedChangeEvent)
                return;

            /*
             * TODO This is a workaround to avoid potential race-conditions when closing a Tab. The problem
             * is that both FEED_CHANGE (due to hiding the tab) and TAB_CLOSE (due to actually closing
             * the tab) get sent when the user closes a tab. The workaround is to delay the processing of
             * TAB_CLOSE a bit to minimize the chance of a race condition.
             */
            int delay = HANDLE_NEWS_SEEN_DELAY;
            if (event == UIEvent.TAB_CLOSE)
                delay += 100;

            JobRunner.runInBackgroundThread(delay, new Runnable() {
                public void run() {

                    /* Application might be in process of closing */
                    if (Controller.getDefault().isShuttingDown())
                        return;

                    /* Check settings if mark as read should be performed */
                    boolean markRead = false;
                    switch (event) {
                    case FEED_CHANGE:
                        markRead = markReadOnFeedChange;
                        break;

                    case TAB_CLOSE:
                        markRead = markReadOnTabClose;
                        break;

                    case MINIMIZE:
                        markRead = markReadOnMinimize;
                        break;
                    }

                    /* Perform the State Change */
                    List<INews> newsToUpdate = new ArrayList<INews>();
                    for (INews newsItem : news) {
                        if (newsItem.getState() == INews.State.NEW)
                            newsToUpdate.add(newsItem);
                        else if (markRead && (newsItem.getState() == INews.State.UPDATED
                                || newsItem.getState() == INews.State.UNREAD))
                            newsToUpdate.add(newsItem);
                    }

                    if (!newsToUpdate.isEmpty()) {

                        /* Force quick update on Feed-Change or Tab Close */
                        if ((event == UIEvent.FEED_CHANGE || event == UIEvent.TAB_CLOSE))
                            Controller.getDefault().getSavedSearchService().forceQuickUpdate();

                        /* Support Undo */
                        UndoStack.getInstance().addOperation(new NewsStateOperation(newsToUpdate,
                                markRead ? INews.State.READ : INews.State.UNREAD, OwlUI.markReadDuplicates()));

                        /* Perform Operation */
                        fNewsDao.setState(newsToUpdate, markRead ? INews.State.READ : INews.State.UNREAD,
                                OwlUI.markReadDuplicates(), false);
                    }

                    /* Retention Strategy */
                    if (inputMark instanceof IBookMark) {

                        /* Ignore currently selected news from retention if changing feeds or minimizing */
                        if (event == UIEvent.FEED_CHANGE || event == UIEvent.MINIMIZE) {
                            if (lastSelection != null && !lastSelection.isEmpty()) {
                                Object obj = lastSelection.getFirstElement();
                                if (obj instanceof INews)
                                    news.remove(obj);
                            }
                        }

                        /* Perform Clean Up */
                        performCleanUp((IBookMark) inputMark, news);
                    }

                    /* Also remember the last selected News */
                    if (event == UIEvent.TAB_CLOSE)
                        rememberSelection(inputMark, lastSelection);
                }
            });
        }
    }

    /*
     * In newspaper and headlines layout there is a chance that the user was not accepting incoming news
     * (by refreshing). In this case, we are not applying any state changes to those news hidden by asking
     * the browser view model for the visible news
     */
    private Collection<INews> filterHidden(Collection<INews> news) {
        if (fLayout == Layout.NEWSPAPER || fLayout == Layout.HEADLINES) {
            NewsBrowserViewModel model = fNewsBrowserControl.getViewer().getViewModel();
            if (model != null) {
                Iterator<INews> iterator = news.iterator();
                while (iterator.hasNext()) {
                    Long id = iterator.next().getId();
                    if (id != null && !model.hasNews(id))
                        iterator.remove();
                }
            }
        }

        return news;
    }

    /**
     * @param news the {@link INews} to check for being part of the browser
     * @return <code>true</code> if the feedview is configured to show headlines
     * or newspaper layout and the news is part of the displayed items and
     * <code>false</code> otherwise.
     */
    public boolean isHidden(INews news) {
        Long id = news.getId();
        return id != null && isHidden(id);
    }

    /**
     * @param reference the {@link NewsReference} to check for being part of the
     * browser
     * @return <code>true</code> if the feedview is configured to show headlines
     * or newspaper layout and the news is part of the displayed items and
     * <code>false</code> otherwise.
     */
    public boolean isHidden(NewsReference reference) {
        return isHidden(reference.getId());
    }

    private boolean isHidden(long newsId) {
        if (fLayout == Layout.NEWSPAPER || fLayout == Layout.HEADLINES) {
            NewsBrowserViewModel model = fNewsBrowserControl.getViewer().getViewModel();
            if (model != null)
                return !model.hasNews(newsId);
        }

        return false;
    }

    /**
     * @param news the {@link INews} to check for being contained in this
     * {@link FeedView}.
     * @return <code>true</code> if this {@link FeedView} contains the given news
     * and <code>false</code> otherwise.
     */
    public boolean contains(INews news) {
        return fContentProvider != null && fContentProvider.hasCachedNews(news);
    }

    /**
     * @return a {@link Collection} of {@link INews} that contains the currently
     * cached news items displayed in the feed view.
     */
    public Collection<INews> getCachedNewsCopy() {
        return fContentProvider != null ? fContentProvider.getCachedNewsCopy() : Collections.<INews>emptyList();
    }

    private void performCleanUp(IBookMark bookmark, Collection<INews> news) {
        if (System.currentTimeMillis() - fLastCleanUpRun.get() > CLEAN_UP_BLOCK_DELAY) {
            RetentionStrategy.process(bookmark, news);
            fLastCleanUpRun.set(System.currentTimeMillis());
        }
    }

    /* React on the Input being set */
    private void onInputSet() {

        /* Check if an action is to be performed */
        PerformAfterInputSet perform = fInput.getPerformOnInputSet();
        perform(perform);

        /* DB Roundtrips done in the background */
        JobRunner.runInBackgroundThread(new Runnable() {
            public void run() {
                if (fInput == null)
                    return;

                IMark mark = fInput.getMark();

                /* Trigger a reload if this is the first time open or previously erroneous open */
                if (mark instanceof IBookMark) {
                    IBookMark bookmark = (IBookMark) mark;
                    if ((bookmark.getLastVisitDate() == null || bookmark.isErrorLoading())
                            && !fContentProvider.hasCachedNews())
                        new ReloadTypesAction(new StructuredSelection(mark), getEditorSite().getShell()).run();
                }

                /* Trigger reload of not loaded included Bookmarks */
                else if (mark instanceof FolderNewsMark) {
                    IFolder folder = ((FolderNewsMark) mark).getFolder();
                    List<IBookMark> bookMarksToReload = new ArrayList<IBookMark>();
                    fillBookMarksToReload(bookMarksToReload, folder);
                    if (!bookMarksToReload.isEmpty())
                        new ReloadTypesAction(new StructuredSelection(bookMarksToReload.toArray()),
                                getEditorSite().getShell()).run();
                }

                /* Mark the Bookmark as visited */
                if (mark instanceof IBookMark)
                    DynamicDAO.getDAO(IBookMarkDAO.class).visited((IBookMark) mark);

                /* Mark the Searchmark as visited */
                else if (mark instanceof ISearchMark)
                    DynamicDAO.getDAO(ISearchMarkDAO.class).visited((ISearchMark) mark);

                /* Mark the newsbin as visited */
                else if (mark instanceof INewsBin)
                    DynamicDAO.getDAO(INewsBinDAO.class).visited((INewsBin) mark);
            }
        });
    }

    /**
     * @param perform the action to perform on this editor.
     */
    public void perform(PerformAfterInputSet perform) {
        if (perform != null) {

            /* Select first News */
            if (perform.getType() == PerformAfterInputSet.Kind.SELECT_FIRST_NEWS) {
                if (fLayout != Layout.NEWSPAPER) //Newspaper will always show a full news on top, so ignore here
                    navigate(false, true, true, false);
            }

            /* Select first unread News */
            else if (perform.getType() == PerformAfterInputSet.Kind.SELECT_UNREAD_NEWS)
                navigate(false, true, true, true);

            /* Select specific News */
            else if (perform.getType() == PerformAfterInputSet.Kind.SELECT_SPECIFIC_NEWS)
                setSelection(new StructuredSelection(perform.getNewsToSelect()));

            /* Make sure to activate this FeedView in case of an action */
            if (perform.shouldActivate())
                fEditorSite.getPage().activate(fEditorSite.getPart());
        }
    }

    private void fillBookMarksToReload(List<IBookMark> bookMarksToReload, IFolder folder) {
        List<IMark> marks = folder.getMarks();
        for (IMark mark : marks) {
            if (mark instanceof IBookMark) {
                if ((((IBookMark) mark).getMostRecentNewsDate() == null))
                    bookMarksToReload.add((IBookMark) mark);
            }
        }

        List<IFolder> childs = folder.getFolders();
        for (IFolder child : childs) {
            fillBookMarksToReload(bookMarksToReload, child);
        }
    }

    /* Set Input to Viewers */
    private void setInput(final INewsMark mark, final boolean reused) {

        /* Update Cache in Background and then apply to UI */
        JobRunner.runUIUpdater(new UIBackgroundJob(fParent) {
            private IProgressMonitor fBgMonitor;

            @Override
            public boolean belongsTo(Object family) {
                return fCacheJobIdentifier.equals(family);
            }

            @Override
            protected void runInBackground(IProgressMonitor monitor) {
                fBgMonitor = monitor;
                if (!monitor.isCanceled())
                    fContentProvider.refreshCache(monitor, mark);
            }

            @Override
            protected void runInUI(IProgressMonitor monitor) {
                IStructuredSelection oldSelection = null;
                IPreferenceScope entityPreferences = Owl.getPreferenceService().getEntityScope(mark);

                long value = entityPreferences.getLong(DefaultPreferences.NM_SELECTED_NEWS);
                if (value > 0) {
                    boolean isListLayout = (OwlUI.getLayout(entityPreferences) == Layout.LIST);
                    boolean openEmptyNews = entityPreferences
                            .getBoolean(DefaultPreferences.BM_OPEN_SITE_FOR_EMPTY_NEWS);
                    boolean openAllNews = entityPreferences.getBoolean(DefaultPreferences.BM_OPEN_SITE_FOR_NEWS);
                    boolean useTransformer = entityPreferences.getBoolean(DefaultPreferences.BM_USE_TRANSFORMER);
                    boolean useExternalBrowser = OwlUI.useExternalBrowser();

                    /* Only re-select if this has not the potential of opening in external Browser */
                    if (!useExternalBrowser || isListLayout || useTransformer || (!openAllNews && !openEmptyNews))
                        oldSelection = new StructuredSelection(new NewsReference(value));
                }

                /* Update Layout */
                if (reused)
                    updateLayout(false);

                /* Hide the Info Bar if it is visible */
                if (reused)
                    fNewsBrowserControl.setInfoBarVisible(false);

                /* Set input to News-Table if Visible */
                if (!fBgMonitor.isCanceled() && isTableViewerVisible())
                    stableSetInputToNewsTable(mark, oldSelection);

                /* Clear old Input from Table */
                else if (!fBgMonitor.isCanceled() && reused)
                    fNewsTableControl.setPartInput(null);

                /* Set input to News-Browser if visible */
                if (!fBgMonitor.isCanceled() && !isTableViewerVisible())
                    fNewsBrowserControl.setPartInput(mark);

                /* Reset old Input to Browser if available */
                else if (!fBgMonitor.isCanceled() && oldSelection != null && isBrowserViewerVisible()) {
                    ISelection selection = fNewsTableControl.getViewer().getSelection();
                    if (!selection.isEmpty()) //Could be filtered
                        fNewsBrowserControl.setPartInput(oldSelection.getFirstElement());
                }

                /* Clear old Input from Browser */
                else if (!fBgMonitor.isCanceled() && reused)
                    fNewsBrowserControl.setPartInput(null);

                /* Update Tab now */
                if (reused)
                    updateTab(fInput);

                /* Handle Input being set now */
                onInputSet();
            }
        });
    }

    /*
     * @see org.eclipse.ui.part.EditorPart#isDirty()
     */
    @Override
    public boolean isDirty() {
        return false;
    }

    /*
     * @see org.eclipse.ui.part.EditorPart#isSaveAsAllowed()
     */
    @Override
    public boolean isSaveAsAllowed() {
        return true;
    }

    /*
     * @see org.eclipse.ui.part.WorkbenchPart#setFocus()
     */
    @Override
    public void setFocus() {

        /* Focus Headlines */
        if (isTableViewerVisible())
            fNewsTableControl.setFocus();

        /* Focus Browser */
        else {
            Runnable runnable = new Runnable() {
                public void run() {
                    fNewsBrowserControl.setFocus();
                }
            };

            /* Run setFocus() delayed if input not yet set */
            Browser browser = fNewsBrowserControl.getViewer().getBrowser().getControl();
            if (!StringUtils.isSet(browser.getUrl()))
                JobRunner.runDelayedInUIThread(browser, runnable);
            else
                runnable.run();
        }
    }

    @Override
    public void dispose() {
        saveSettings();
        unregisterListeners();

        super.dispose();
        fContentProvider.dispose();
        fNewsTableControl.dispose();
        fNewsBrowserControl.dispose();
        fResourceManager.dispose();
        fIsDisposed = true;
    }

    private void unregisterListeners() {
        fEditorSite.getPage().removePartListener(fPartListener);
        DynamicDAO.removeEntityListener(IBookMark.class, fBookMarkListener);
        DynamicDAO.removeEntityListener(IFolder.class, fFolderListener);
        DynamicDAO.removeEntityListener(ISearchMark.class, fSearchMarkListener);
        DynamicDAO.removeEntityListener(IFeed.class, fFeedListener);
        DynamicDAO.removeEntityListener(ISearchCondition.class, fSearchConditionListener);
        DynamicDAO.removeEntityListener(INewsBin.class, fNewsBinListener);
        Controller.getDefault().removeBookMarkLoadListener(fBookMarkLoadListener);
    }

    /**
     * Update the Layout in the Feed View.
     */
    public void updateLayout() {
        fRootComposite.setRedraw(false);
        try {
            updateLayout(true);
        } finally {
            fRootComposite.setRedraw(true);
        }
    }

    private void updateLayout(boolean updateInput) {
        IPreferenceScope preferences = Owl.getPreferenceService().getEntityScope(fInput.getMark());
        Layout layout = OwlUI.getLayout(preferences);

        /* Return early if layout already up to date */
        if (fLayout == layout)
            return;

        /* Notify Toolbar */
        fFilterBar.doLayout(layout, false);

        /* Notify Controls */
        fNewsTableControl.onLayoutChanged(layout);
        fNewsBrowserControl.onLayoutChanged(layout);

        /* Classic Layout (default) */
        if (layout == Layout.CLASSIC) {
            restoreTable(updateInput);
            fSashForm.setOrientation(SWT.VERTICAL);
        }

        /* Vertical Layout */
        else if (layout == Layout.VERTICAL) {
            restoreTable(updateInput);
            fSashForm.setOrientation(SWT.HORIZONTAL);
        }

        /* List Layout */
        else if (layout == Layout.LIST) {
            maximizeTable(updateInput);
        }

        /* Newspaper / Headlines Layout */
        else if (layout == Layout.NEWSPAPER || layout == Layout.HEADLINES) {
            maximizeBrowser(updateInput);
            if (updateInput && (fLayout == Layout.NEWSPAPER || fLayout == Layout.HEADLINES))
                refreshBrowserViewer(); //A change between Newspaper and Headlines needs a refresh due to the different CSS
        }

        /* Update Separators */
        updateSeparators(layout);

        /* Hide the Info Bar if it is visible */
        fNewsBrowserControl.setInfoBarVisible(false);

        /* Layout */
        fNewsTableControlContainer.layout();
        fBrowserViewerControlContainer.layout();

        /* Remember Layout */
        fLayout = layout;
    }

    private void maximizeTable(boolean updateInput) {
        Control maximizedControl = fSashForm.getMaximizedControl();
        if (fNewsTableControlContainer.equals(maximizedControl))
            return;

        fSashForm.setMaximizedControl(fNewsTableControlContainer);

        if (updateInput) {
            if (fBrowserViewerControlContainer.equals(maximizedControl)) {
                fNewsTableControl.setPartInput(fInput.getMark());
                fNewsTableControl.adjustScrollPosition();
                if (fNewsGrouping.getType() != NewsGrouping.Type.NO_GROUPING)
                    expandNewsTableViewerGroups(true, StructuredSelection.EMPTY);
            }
            fNewsBrowserControl.setPartInput(null);
        }

        fNewsTableControl.setFocus();
    }

    private void maximizeBrowser(boolean updateInput) {
        Control maximizedControl = fSashForm.getMaximizedControl();
        if (fBrowserViewerControlContainer.equals(maximizedControl))
            return;

        fSashForm.setMaximizedControl(fBrowserViewerControlContainer);

        if (updateInput) {
            fNewsTableControl.getViewer().setSelection(StructuredSelection.EMPTY);
            fNewsBrowserControl.setPartInput(fInput.getMark());
            fNewsTableControl.setPartInput(null);
        }

        fNewsBrowserControl.setFocus();
    }

    private void restoreTable(boolean updateInput) {
        Control maximizedControl = fSashForm.getMaximizedControl();
        if (maximizedControl == null)
            return;

        fSashForm.setMaximizedControl(null);

        if (updateInput) {
            fNewsTableControl.setPartInput(fInput.getMark());
            fNewsTableControl.adjustScrollPosition();
            if (fNewsGrouping.getType() != NewsGrouping.Type.NO_GROUPING)
                expandNewsTableViewerGroups(true, StructuredSelection.EMPTY);
            fNewsBrowserControl.setPartInput(null);
        }

        fNewsTableControl.setFocus();
    }

    private void updateSeparators(Layout layout) {

        /* Table Separators */
        boolean showFilterTableSeparator = false;
        if (!Application.IS_MAC || layout != Layout.CLASSIC)
            showFilterTableSeparator = fFilterBar.isVisible();
        ((GridData) fHorizontalFilterTableSep.getLayoutData()).exclude = !showFilterTableSeparator;
        ((GridData) fVerticalTableBrowserSep.getLayoutData()).exclude = (layout != Layout.VERTICAL);
        ((GridData) fHorizontalTableBrowserSep.getLayoutData()).exclude = (layout != Layout.CLASSIC);

        /* Browser Separators */
        ((GridData) fVerticalBrowserSep.getLayoutData()).exclude = (layout != Layout.VERTICAL);

        /* Horizontal Layout */
        if (layout == Layout.CLASSIC) {
            fHorizontalBrowserSep.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false, 2, 1));
            ((GridData) fHorizontalBrowserSep.getLayoutData()).exclude = false;
        }

        /* Verical Layout */
        else if (layout == Layout.VERTICAL) {
            fHorizontalBrowserSep.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false, 1, 1));
            ((GridData) fHorizontalBrowserSep.getLayoutData()).exclude = !fBrowserBar.isVisible();
        }

        /* Newspaper / Headlines Layout */
        else if (layout == Layout.NEWSPAPER || layout == Layout.HEADLINES) {
            fHorizontalBrowserSep.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false, 2, 1));
            ((GridData) fHorizontalBrowserSep.getLayoutData()).exclude = !fBrowserBar.isVisible();
        }

        /* Update Visibility based on Layout Data */
        fHorizontalFilterTableSep.setVisible(!((GridData) fHorizontalFilterTableSep.getLayoutData()).exclude);
        fVerticalTableBrowserSep.setVisible(!((GridData) fVerticalTableBrowserSep.getLayoutData()).exclude);
        fHorizontalTableBrowserSep.setVisible(!((GridData) fHorizontalTableBrowserSep.getLayoutData()).exclude);
        fVerticalBrowserSep.setVisible(!((GridData) fVerticalBrowserSep.getLayoutData()).exclude);
        fHorizontalBrowserSep.setVisible(!((GridData) fHorizontalBrowserSep.getLayoutData()).exclude);
    }

    /**
     * Update the visibility of the filter bar and browser bar.
     */
    public void updateToolbarVisibility() {
        fFilterBar.updateVisibility();
        fBrowserBar.updateVisibility();
        updateSeparators(fLayout);
        fRootComposite.layout(true, true);
    }

    /**
     * Toggle between newspaper and classic/vertical layout.
     */
    public void toggleNewspaperLayout() {

        /* Lookup current layout from actual UI to support toggling */
        boolean isNewspaperLayout = !isTableViewerVisible();
        boolean isClassicLayout = (fSashForm.getOrientation() & SWT.VERTICAL) != 0;

        /* Determine new layout based on existing */
        Layout newLayout = isNewspaperLayout ? (isClassicLayout ? Layout.CLASSIC : Layout.VERTICAL)
                : Layout.NEWSPAPER;

        /* Save only into Entity if the Entity was configured with the given Settings before */
        FeedViewInput input = ((FeedViewInput) getEditorInput());
        IPreferenceScope entityPreferences;
        if (input.getMark() instanceof FolderNewsMark)
            entityPreferences = Owl.getPreferenceService()
                    .getEntityScope(((FolderNewsMark) input.getMark()).getFolder());
        else
            entityPreferences = Owl.getPreferenceService().getEntityScope(input.getMark());

        if (entityPreferences.hasKey(DefaultPreferences.FV_LAYOUT)) {
            entityPreferences.putInteger(DefaultPreferences.FV_LAYOUT, newLayout.ordinal());
            entityPreferences.flush();

            /* Update Layout (on current feed view) */
            updateLayout();
        }

        /* Save Globally */
        else {
            fPreferences.putInteger(DefaultPreferences.FV_LAYOUT, newLayout.ordinal());

            /* Update Layout (on all opened feed views) */
            EditorUtils.updateLayout();
        }
    }

    /**
     * Refreshes all parts of this editor.
     *
     * @param delayRedraw If <code>TRUE</code> delay redraw until operation is
     * done.
     * @param updateLabels If <code>TRUE</code> update all Labels.
     */
    void refresh(boolean delayRedraw, boolean updateLabels) {
        refreshTableViewer(delayRedraw, updateLabels);
        refreshBrowserViewer();
    }

    /**
     * A special key was pressed from the Quicksearch Input-Field. Handle it.
     *
     * @param traversal The Traversal that occured from the quicksearch.
     * @param clear If <code>true</code> indicates that the quicksearch was
     * cleared.
     */
    void handleQuicksearchTraversalEvent(int traversal, boolean clear) {

        /* Enter was hit */
        if ((traversal & SWT.TRAVERSE_RETURN) != 0) {

            /* Select and Focus TreeViewer */
            if (isTableViewerVisible()) {
                Tree tree = (Tree) fNewsTableControl.getViewer().getControl();
                if (tree.getItemCount() > 0) {
                    IStructuredSelection lastSelection = fNewsTableControl.getLastNonEmptySelection();
                    if (lastSelection.isEmpty() || !clear) //When not clearing, select the first result from the list
                        lastSelection = new StructuredSelection(tree.getItem(0).getData());

                    fNewsTableControl.getViewer().setSelection(lastSelection);
                    fNewsTableControl.setFocus();
                }
            }

            /* Move Focus into BrowserViewer */
            else {
                fNewsBrowserControl.setFocus();
            }
        }

        /* Page Up / Down was hit */
        else if ((traversal & SWT.TRAVERSE_PAGE_NEXT) != 0 || (traversal & SWT.TRAVERSE_PAGE_PREVIOUS) != 0) {
            setFocus();
        }
    }

    /* Refresh Table-Viewer if visible */
    void refreshTableViewer(boolean delayRedraw, boolean updateLabels) {

        /* Return on Shutdown */
        if (Controller.getDefault().isShuttingDown())
            return;

        /* Only if Table Viewer is visible */
        if (isTableViewerVisible()) {
            boolean groupingEnabled = fNewsGrouping.getType() != NewsGrouping.Type.NO_GROUPING;

            /* Remember Selection if grouping enabled */
            ISelection selection = StructuredSelection.EMPTY;
            if (groupingEnabled)
                selection = fNewsTableControl.getViewer().getSelection();

            /* Delay redraw operations if requested */
            if (delayRedraw)
                fNewsTableControl.getViewer().getControl().getParent().setRedraw(false);
            try {

                /* Refresh */
                fNewsTableControl.getViewer().refresh(updateLabels);

                /* Expand all Groups if grouping is enabled */
                if (groupingEnabled)
                    expandNewsTableViewerGroups(false, selection);
            }

            /* Redraw now if delayed before */
            finally {
                if (delayRedraw)
                    fNewsTableControl.getViewer().getControl().getParent().setRedraw(true);
            }
        }
    }

    private void expandNewsTableViewerGroups(boolean delayRedraw, ISelection oldSelection) {
        TreeViewer viewer = fNewsTableControl.getViewer();
        Tree tree = (Tree) viewer.getControl();

        /* Remember TopItem if required */
        TreeItem topItem = oldSelection.isEmpty() ? tree.getTopItem() : null;

        /* Expand All & Restore Selection with redraw false */
        if (delayRedraw)
            tree.getParent().setRedraw(false);
        try {
            viewer.expandAll();

            /* Restore selection if required */
            if (!oldSelection.isEmpty() && viewer.getSelection().isEmpty())
                viewer.setSelection(oldSelection, true);
            else if (topItem != null)
                tree.setTopItem(topItem);
        } finally {
            if (delayRedraw)
                tree.getParent().setRedraw(true);
        }
    }

    /* TODO This is a Workaround until Eclipse Bug #159586 is fixed */
    private void stableSetInputToNewsTable(Object input, ISelection oldSelection) {
        TreeViewer viewer = fNewsTableControl.getViewer();
        Tree tree = (Tree) viewer.getControl();

        /* Set Input & Restore Selection with redraw false */
        tree.getParent().setRedraw(false);
        try {
            fNewsTableControl.setPartInput(input);

            /* Restore selection if required */
            if (oldSelection != null) {
                fNewsTableControl.setBlockNewsStateTracker(true);
                try {
                    viewer.setSelection(oldSelection);
                } finally {
                    fNewsTableControl.setBlockNewsStateTracker(false);
                }
            }

            /* Adjust Scroll Position */
            fNewsTableControl.adjustScrollPosition();
        } finally {
            tree.getParent().setRedraw(true);
        }
    }

    private void rememberSelection(final IMark inputMark, final IStructuredSelection selection) {
        SafeRunnable.run(new LoggingSafeRunnable() {
            public void run() throws Exception {
                IPreferenceScope inputPrefs = Owl.getPreferenceService().getEntityScope(inputMark);
                long oldSelectionValue = inputPrefs.getLong(DefaultPreferences.NM_SELECTED_NEWS);

                /* Find Selected News ID */
                long newSelectionValue = 0;
                if (!selection.isEmpty()) {
                    Object obj = selection.getFirstElement();
                    if (obj instanceof INews)
                        newSelectionValue = ((INews) obj).getId();
                }

                boolean needToSave = false;

                /* Selection Provided */
                if (newSelectionValue > 0) {
                    if (oldSelectionValue != newSelectionValue) {
                        needToSave = true;
                        inputPrefs.putLong(DefaultPreferences.NM_SELECTED_NEWS, newSelectionValue);
                    }
                }

                /* No Selection Provided */
                else {
                    if (oldSelectionValue > 0) {
                        needToSave = true;
                        inputPrefs.delete(DefaultPreferences.NM_SELECTED_NEWS);
                    }
                }

                IEntity entityToSave;
                if (fInput.getMark() instanceof FolderNewsMark)
                    entityToSave = ((FolderNewsMark) fInput.getMark()).getFolder();
                else
                    entityToSave = fInput.getMark();

                if (needToSave)
                    DynamicDAO.save(entityToSave);
            }
        });
    }

    /* Refresh Browser-Viewer */
    void refreshBrowserViewer() {

        /* Return on Shutdown */
        if (Controller.getDefault().isShuttingDown())
            return;

        /* Refresh if browser is visible */
        if (isBrowserViewerVisible())
            fNewsBrowserControl.getViewer().refresh();
    }

    /**
     * Check wether the News-Table-Part of this Editor is visible or not
     * (minmized).
     *
     * @return TRUE if the News-Table-Part is visible, FALSE otherwise.
     */
    boolean isTableViewerVisible() {
        return fSashForm.getMaximizedControl() == null
                || fSashForm.getMaximizedControl() == fNewsTableControlContainer;
    }

    /**
     * Check wether the Browser-Part of this Editor is visible or not (minmized).
     *
     * @return TRUE if the Browser-Table-Part is visible, FALSE otherwise.
     */
    boolean isBrowserViewerVisible() {
        return fSashForm.getMaximizedControl() == null
                || fSashForm.getMaximizedControl() == fBrowserViewerControlContainer;
    }

    /**
     * Get the shared ViewerFilter used to filter News.
     *
     * @return the shared ViewerFilter used to filter News.
     */
    NewsFilter getFilter() {
        return fNewsFilter;
    }

    /**
     * Get the ViewerComparator that is used for sorting news.
     *
     * @return the {@link ViewerComparator} for sorting news.
     */
    NewsComparator getComparator() {
        if (isTableViewerVisible())
            return (NewsComparator) fNewsTableControl.getViewer().getComparator();

        return (NewsComparator) fNewsBrowserControl.getViewer().getComparator();
    }

    /**
     * Get the shared Viewer-Grouper used to group News.
     *
     * @return the shared Viewer-Grouper used to group News.
     */
    NewsGrouping getGrouper() {
        return fNewsGrouping;
    }

    NewsBrowserControl getNewsBrowserControl() {
        return fNewsBrowserControl;
    }

    NewsTableControl getNewsTableControl() {
        return fNewsTableControl;
    }

    /*
     * @see org.eclipse.ui.part.WorkbenchPart#createPartControl(org.eclipse.swt.widgets.Composite)
     */
    @Override
    public void createPartControl(Composite parent) {
        fCreated = true;
        fParent = parent;

        /* Shared Viewer Helper */
        fNewsFilter = new NewsFilter();
        fNewsFilter.setType(fInitialFilterType);
        fNewsFilter.setSearchTarget(fInitialSearchTarget);
        fNewsFilter.setNewsMark(fInput.getMark());

        fNewsGrouping = new NewsGrouping();
        fNewsGrouping.setType(fInitialGroupType);

        /* Top-Most root Composite in Editor */
        fRootComposite = new Composite(fParent, SWT.NONE);
        fRootComposite.setLayout(LayoutUtils.createGridLayout(1, 0, 0));
        ((GridLayout) fRootComposite.getLayout()).verticalSpacing = 0;

        /* FilterBar */
        fFilterBar = new FilterBar(this, fRootComposite);

        /* Separate Filter from Table */
        boolean showSeparator = false;
        if (!Application.IS_MAC || fLayout != Layout.CLASSIC)
            showSeparator = fFilterBar.isVisible();
        fHorizontalFilterTableSep = new Label(fRootComposite, SWT.SEPARATOR | SWT.HORIZONTAL);
        fHorizontalFilterTableSep.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false));
        ((GridData) fHorizontalFilterTableSep.getLayoutData()).exclude = !showSeparator;
        fHorizontalFilterTableSep.setVisible(showSeparator);

        /* SashForm dividing Feed and News View */
        boolean useClassicLayout = (fLayout != Layout.VERTICAL);
        fSashForm = new SashForm(fRootComposite, (useClassicLayout ? SWT.VERTICAL : SWT.HORIZONTAL) | SWT.SMOOTH);
        fSashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

        /* Table-Viewer to display headlines */
        NewsTableViewer tableViewer;
        {
            fNewsTableControlContainer = new Composite(fSashForm, SWT.None);
            fNewsTableControlContainer.setLayout(LayoutUtils.createGridLayout(2, 0, 0, 0, 0, false));
            fNewsTableControlContainer.addControlListener(new ControlAdapter() {
                @Override
                public void controlResized(ControlEvent e) {
                    fCacheWeights = fSashForm.getWeights();
                }
            });

            fNewsTableControl = new NewsTableControl();
            fNewsTableControl.init(fFeedViewSite);
            fNewsTableControl.onInputChanged(fInput);

            /* Create Viewer */
            fNewsTableControl.createPart(fNewsTableControlContainer);
            tableViewer = fNewsTableControl.getViewer();

            /* Clear any quicksearch when ESC is hit from the Tree */
            tableViewer.getControl().addKeyListener(new KeyAdapter() {
                @Override
                public void keyPressed(KeyEvent e) {
                    if (e.keyCode == SWT.ESC)
                        fFilterBar.clearQuickSearch(true);
                }
            });

            /* Separate from Browser-Viewer (Vertically) */
            fVerticalTableBrowserSep = new Label(fNewsTableControlContainer, SWT.SEPARATOR | SWT.VERTICAL);
            fVerticalTableBrowserSep.setLayoutData(new GridData(SWT.BEGINNING, SWT.FILL, false, false));
            ((GridData) fVerticalTableBrowserSep.getLayoutData()).exclude = (fLayout != Layout.VERTICAL);

            /* Separate from Browser-Viewer (Horizontally) */
            fHorizontalTableBrowserSep = new Label(fNewsTableControlContainer, SWT.SEPARATOR | SWT.HORIZONTAL);
            fHorizontalTableBrowserSep.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false, 2, 1));
            ((GridData) fHorizontalTableBrowserSep.getLayoutData()).exclude = (fLayout != Layout.CLASSIC);
        }

        /* Browser-Viewer to display news */
        NewsBrowserViewer browserViewer;
        {
            fBrowserViewerControlContainer = new Composite(fSashForm, SWT.None);
            fBrowserViewerControlContainer.setLayout(LayoutUtils.createGridLayout(2, 0, 0, 0, 0, false));
            fBrowserViewerControlContainer
                    .setBackground(parent.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND));

            /* Separate to Browser (Vertically) */
            fVerticalBrowserSep = new Label(fBrowserViewerControlContainer, SWT.SEPARATOR | SWT.VERTICAL);
            fVerticalBrowserSep.setLayoutData(new GridData(SWT.BEGINNING, SWT.FILL, false, false, 1, 3));
            ((GridData) fVerticalBrowserSep.getLayoutData()).exclude = (fLayout != Layout.VERTICAL);

            /* Browser Bar for Navigation */
            fBrowserBar = new BrowserBar(this, fBrowserViewerControlContainer);

            /* Separate to Browser (Horizontally) */
            fHorizontalBrowserSep = new Label(fBrowserViewerControlContainer, SWT.SEPARATOR | SWT.HORIZONTAL);

            /* Horizontal Layout */
            if (fLayout == Layout.CLASSIC) {
                fHorizontalBrowserSep.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false, 2, 1));
                ((GridData) fHorizontalBrowserSep.getLayoutData()).exclude = false;
            }

            /* Verical Layout */
            else if (fLayout == Layout.VERTICAL) {
                fHorizontalBrowserSep.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false, 1, 1));
                ((GridData) fHorizontalBrowserSep.getLayoutData()).exclude = !fBrowserBar.isVisible();
            }

            /* Browser Maximized */
            else if (fLayout == Layout.NEWSPAPER || fLayout == Layout.HEADLINES) {
                fHorizontalBrowserSep.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false, 2, 1));
                ((GridData) fHorizontalBrowserSep.getLayoutData()).exclude = !fBrowserBar.isVisible();
            }

            fNewsBrowserControl = new NewsBrowserControl();
            fNewsBrowserControl.init(fFeedViewSite);
            fNewsBrowserControl.onInputChanged(fInput);

            /* Create Viewer */
            fNewsBrowserControl.createPart(fBrowserViewerControlContainer);
            browserViewer = fNewsBrowserControl.getViewer();

            /* Clear any quicksearch when ESC is hit from the Tree */
            browserViewer.getControl().addKeyListener(new KeyAdapter() {
                @Override
                public void keyPressed(KeyEvent e) {
                    if (e.keyCode == SWT.ESC)
                        fFilterBar.clearQuickSearch(true);
                }
            });

            /* Init the Browser Bar with the CBrowser */
            fBrowserBar.init(browserViewer.getBrowser());
        }

        /* SashForm weights */
        fSashForm.setWeights(fInitialWeights);
        if (fLayout == Layout.NEWSPAPER || fLayout == Layout.HEADLINES)
            fSashForm.setMaximizedControl(fBrowserViewerControlContainer);
        else if (fLayout == Layout.LIST)
            fSashForm.setMaximizedControl(fNewsTableControlContainer);

        /* Create the shared Content-Provider */
        fContentProvider = new NewsContentProvider(tableViewer, browserViewer, this);

        /* Init all Viewers */
        fNewsTableControl.initViewer(fContentProvider, fNewsFilter);
        fNewsBrowserControl.initViewer(fContentProvider, fNewsFilter);

        /* Set Input to Viewers */
        setInput(fInput.getMark(), false);
    }

    /*
     * This method is currently only being used when the news filter was changed and
     * the input is of type news bin or search mark. In this case, the cache is clever
     * enough to not resolve all news, but only the ones necessary from the used filter.
     *
     * However, if the filter changes, the feed view needs to ensure the cache is up to
     * date in case more elements are now visible. Thus, the cache is refreshed, but
     * only for added news that have not been there previously.
     */
    void revalidateCaches() {
        fContentProvider.refreshCache(null, fInput.getMark());
    }

    /**
     * Navigate to the next/previous read or unread News respecting the News-Items
     * that are displayed in the NewsTableControl.
     *
     * @param respectSelection If <code>TRUE</code>, respect the current selected
     * Item from the Tree as starting-node for the navigation, or
     * <code>FALSE</code> otherwise.
     * @param onInputSet if <code>true</code> this method is called directly after
     * an input was set, <code>false</code> otherwise.
     * @param next If <code>TRUE</code>, move to the next item, or previous if
     * <code>FALSE</code>.
     * @param unread If <code>TRUE</code>, only move to unread items, or ignore if
     * <code>FALSE</code>.
     * @return Returns <code>TRUE</code> in case navigation found a valid item, or
     * <code>FALSE</code> otherwise.
     */
    public boolean navigate(boolean respectSelection, final boolean onInputSet, final boolean next,
            final boolean unread) {

        /* Check for unread counter */
        if (unread && fInput.getMark()
                .getNewsCount(EnumSet.of(INews.State.NEW, INews.State.UNREAD, INews.State.UPDATED)) == 0)
            return false;

        /* Navigate in maximized Browser */
        if (!isTableViewerVisible()) {

            /* Delay navigation because input was just set and browser needs a little to render */
            if (onInputSet) {
                JobRunner.runInUIThread(BROWSER_OPERATIONS_DELAY, fNewsBrowserControl.getViewer().getControl(),
                        new Runnable() {
                            public void run() {
                                fNewsBrowserControl.getViewer().navigate(next, unread, onInputSet);
                            }
                        });
            }

            /* Directly Navigate */
            else
                fNewsBrowserControl.getViewer().navigate(next, unread, onInputSet);

            return true;
        }

        Tree newsTree = fNewsTableControl.getViewer().getTree();

        /* Nothing to Navigate to */
        if (newsTree.getItemCount() == 0 || newsTree.isDisposed())
            return false;

        /* Navigate */
        return navigate(newsTree, respectSelection, next, unread);
    }

    private boolean navigate(Tree tree, boolean respectSelection, boolean next, boolean unread) {

        /* Selection is Present */
        if (respectSelection && tree.getSelectionCount() > 0) {

            /* Try navigating from Selection */
            ITreeNode startingNode = new WidgetTreeNode(tree.getSelection()[0], fNewsTableControl.getViewer());
            if (navigate(startingNode, next, unread))
                return true;
        }

        /* No Selection is Present */
        else {
            ITreeNode startingNode = new WidgetTreeNode(tree, fNewsTableControl.getViewer());
            return navigate(startingNode, true, unread);
        }

        return false;
    }

    private boolean navigate(ITreeNode startingNode, boolean next, final boolean unread) {

        /* Create Traverse-Helper */
        TreeTraversal traverse = new TreeTraversal(startingNode) {
            @Override
            public boolean select(ITreeNode node) {
                return isValidNavigation(node, unread);
            }
        };

        /* Retrieve and select new Target Node */
        ITreeNode targetNode = (next ? traverse.nextNode() : traverse.previousNode());
        if (targetNode != null) {
            ISelection selection = new StructuredSelection(targetNode.getData());
            fNewsTableControl.getViewer().setSelection(selection, true);
            return true;
        }

        return false;
    }

    private boolean isValidNavigation(ITreeNode node, boolean unread) {
        Object data = node.getData();

        /* Require a News */
        if (!(data instanceof INews))
            return false;

        /* Check if News is unread if set as flag */
        INews news = (INews) data;
        if (unread && !CoreUtils.isUnread(news.getState()))
            return false;

        return true;
    }

    /**
     * @return <code>true</code> if this feedview is currently visible or
     * <code>false</code> otherwise.
     */
    public boolean isVisible() {
        return fEditorSite.getPage().isPartVisible(fEditorSite.getPart());
    }

    /**
     * Returns the <code>Composite</code> that is the Parent Control of this
     * Editor Part.
     *
     * @return The <code>Composite</code> that is the Parent Control of this
     * Editor Part.
     */
    Composite getEditorControl() {
        return fParent;
    }

    /**
     * @param blockFeedChangeEvent <code>true</code> to block the processing of
     * feed change events and <code>false</code> otherwise.
     */
    public static void setBlockFeedChangeEvent(boolean blockFeedChangeEvent) {
        fgBlockFeedChangeEvent = blockFeedChangeEvent;
    }

    /**
     * @return <code>true</code> if the news browser viewer of this feed view is
     * showing the contents of a website and <code>false</code> otherwise.
     */
    public boolean isBrowserShowingNews() {
        if (fNewsBrowserControl != null && fNewsBrowserControl.getViewer() != null) {
            if (isBrowserViewerVisible()) {
                CBrowser browser = fNewsBrowserControl.getViewer().getBrowser();
                if (browser != null && browser.getControl() != null && !browser.getControl().isDisposed()) {
                    String url = browser.getControl().getUrl();
                    return StringUtils.isSet(url) && ApplicationServer.getDefault().isNewsServerUrl(url);
                }
            }
        }

        return false;
    }
}