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

Java tutorial

Introduction

Here is the source code for org.rssowl.ui.internal.editors.feed.NewsTableControl.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.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.GroupMarker;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.LocalResourceManager;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.LocalSelectionTransfer;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.ColumnViewer;
import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IContentProvider;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.IElementComparer;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewerColumn;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.jface.window.SameShellProvider;
import org.eclipse.jface.window.ToolTip;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DragSourceListener;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.dnd.URLTransfer;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.PlatformUI;
import org.rssowl.core.Owl;
import org.rssowl.core.internal.persist.pref.DefaultPreferences;
import org.rssowl.core.persist.IEntity;
import org.rssowl.core.persist.IFolderChild;
import org.rssowl.core.persist.ILabel;
import org.rssowl.core.persist.INews;
import org.rssowl.core.persist.INewsBin;
import org.rssowl.core.persist.INewsMark;
import org.rssowl.core.persist.ISearchMark;
import org.rssowl.core.persist.dao.DynamicDAO;
import org.rssowl.core.persist.dao.INewsDAO;
import org.rssowl.core.persist.event.LabelAdapter;
import org.rssowl.core.persist.event.LabelEvent;
import org.rssowl.core.persist.pref.IPreferenceScope;
import org.rssowl.core.persist.reference.ModelReference;
import org.rssowl.core.persist.reference.NewsBinReference;
import org.rssowl.core.persist.reference.SearchMarkReference;
import org.rssowl.core.util.CoreUtils;
import org.rssowl.core.util.ITask;
import org.rssowl.core.util.LoggingSafeRunnable;
import org.rssowl.core.util.StringUtils;
import org.rssowl.core.util.TaskAdapter;
import org.rssowl.ui.internal.ApplicationActionBarAdvisor;
import org.rssowl.ui.internal.ApplicationWorkbenchWindowAdvisor;
import org.rssowl.ui.internal.EntityGroup;
import org.rssowl.ui.internal.FolderNewsMark;
import org.rssowl.ui.internal.OwlUI;
import org.rssowl.ui.internal.OwlUI.Layout;
import org.rssowl.ui.internal.StatusLineUpdater;
import org.rssowl.ui.internal.actions.ArchiveNewsAction;
import org.rssowl.ui.internal.actions.AutomateFilterAction;
import org.rssowl.ui.internal.actions.CreateFilterAction.PresetAction;
import org.rssowl.ui.internal.actions.MakeNewsStickyAction;
import org.rssowl.ui.internal.actions.MarkAllNewsReadAction;
import org.rssowl.ui.internal.actions.MoveCopyNewsToBinAction;
import org.rssowl.ui.internal.actions.OpenInBrowserAction;
import org.rssowl.ui.internal.actions.OpenInExternalBrowserAction;
import org.rssowl.ui.internal.actions.OpenNewsAction;
import org.rssowl.ui.internal.actions.ToggleReadStateAction;
import org.rssowl.ui.internal.editors.browser.WebBrowserContext;
import org.rssowl.ui.internal.undo.NewsStateOperation;
import org.rssowl.ui.internal.undo.UndoStack;
import org.rssowl.ui.internal.util.CTree;
import org.rssowl.ui.internal.util.JobRunner;
import org.rssowl.ui.internal.util.JobTracker;
import org.rssowl.ui.internal.util.ModelUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Part of the FeedView to display News in a TableViewer.
 *
 * @author bpasero
 */
public class NewsTableControl implements IFeedViewPart {

    /* Tracker to Mark selected news as Read */
    private class MarkReadTracker extends JobTracker {
        private boolean fUpdateDelayDynamically;

        MarkReadTracker(int delay, boolean showProgress) {
            super(delay, showProgress, ITask.Priority.INTERACTIVE);
        }

        @Override
        public int getDelay() {
            if (fUpdateDelayDynamically)
                return fInputPreferences.getInteger(DefaultPreferences.MARK_READ_IN_MILLIS);

            return super.getDelay();
        }

        public void setUpdateDelayDynamically(boolean updateDelayDynamically) {
            fUpdateDelayDynamically = updateDelayDynamically;
        }
    }

    /* Custom Tooltip Support for Feed Column */
    private static class FeedColumnToolTipSupport extends ColumnViewerToolTipSupport {
        FeedColumnToolTipSupport(ColumnViewer viewer, int style) {
            super(viewer, style, false);
        }

        /*
         * @see org.eclipse.jface.viewers.ColumnViewerToolTipSupport#getToolTipArea(org.eclipse.swt.widgets.Event)
         */
        @Override
        protected Object getToolTipArea(Event event) {
            Tree tree = (Tree) event.widget;
            Point point = new Point(event.x, event.y);
            TreeItem item = tree.getItem(point);

            /* Only valid for Feed Column */
            if (item != null) {
                int feedIndex = indexOf(tree, NewsColumn.FEED);
                if (feedIndex >= 0 && item.getBounds(feedIndex).contains(point))
                    return super.getToolTipArea(event);
            }

            return null;
        }

        private static int indexOf(Tree tree, NewsColumn column) {
            if (tree.isDisposed())
                return -1;

            TreeColumn[] columns = tree.getColumns();
            for (int i = 0; i < columns.length; i++) {
                if (column == columns[i].getData(NewsColumnViewModel.COL_ID))
                    return i;
            }

            return -1;
        }

        public static void enableFor(ColumnViewer viewer) {
            new FeedColumnToolTipSupport(viewer, ToolTip.NO_RECREATE);
        }
    }

    private IFeedViewSite fFeedViewSite;
    private MarkReadTracker fNewsStateTracker;
    private MarkReadTracker fInstantMarkUnreadTracker;
    private NewsTableViewer fViewer;
    private NewsTableLabelProvider fNewsTableLabelProvider;
    private ISelectionChangedListener fSelectionChangeListener;
    private IPropertyChangeListener fPropertyChangeListener;
    private CTree fCustomTree;
    private int[] fOldColumnOrder;
    private LocalResourceManager fResources;
    private NewsComparator fNewsSorter;
    private Cursor fHandCursor;
    private boolean fShowsHandCursor;
    private final AtomicBoolean fBlockNewsStateTracker = new AtomicBoolean(false);
    private LabelAdapter fLabelListener;
    private IPreferenceScope fInputPreferences;
    private final INewsDAO fNewsDao = Owl.getPersistenceService().getDAOService().getNewsDAO();
    private NewsColumnViewModel fColumnModel;
    private FeedViewInput fEditorInput;
    private boolean fBlockColumMoveEvent;
    private IStructuredSelection fLastSelection = StructuredSelection.EMPTY;
    private IStructuredSelection fLastNonEmptySelection = StructuredSelection.EMPTY;
    private long fLastColumnActionInvokedMillies;
    private Menu fAttachmentsMenu;

    /* Settings */
    private IPreferenceScope fGlobalPreferences;

    /*
     * @see org.rssowl.ui.internal.editors.feed.IFeedViewPart#init(org.rssowl.ui.internal.editors.feed.IFeedViewSite)
     */
    public void init(IFeedViewSite feedViewSite) {
        fFeedViewSite = feedViewSite;
        fGlobalPreferences = Owl.getPreferenceService().getGlobalScope();
        fResources = new LocalResourceManager(JFaceResources.getResources());
        fInstantMarkUnreadTracker = new MarkReadTracker(0, false);
    }

    /*
     * @see org.rssowl.ui.internal.editors.feed.IFeedViewPart#onInputChanged(org.rssowl.ui.internal.editors.feed.FeedViewInput)
     */
    public void onInputChanged(FeedViewInput input) {
        fEditorInput = input;
        fInputPreferences = Owl.getPreferenceService().getEntityScope(input.getMark());

        if (fNewsStateTracker != null)
            fNewsStateTracker.cancel();

        fInstantMarkUnreadTracker.cancel();

        fNewsStateTracker = new MarkReadTracker(
                fInputPreferences.getInteger(DefaultPreferences.MARK_READ_IN_MILLIS), false);
        fNewsStateTracker.setUpdateDelayDynamically(true);
    }

    /*
     * @see org.rssowl.ui.internal.editors.feed.IFeedViewPart#onLayoutChanged(org.rssowl.ui.internal.OwlUI.Layout)
     */
    public void onLayoutChanged(Layout newLayout) {
    }

    /*
     * @see org.rssowl.ui.internal.editors.feed.IFeedViewPart#createPart(org.eclipse.swt.widgets.Composite)
     */
    public void createPart(Composite parent) {
        int style = SWT.MULTI | SWT.FULL_SELECTION;

        fCustomTree = new CTree(parent, style);
        fCustomTree.getControl().setHeaderVisible(true);

        fViewer = new NewsTableViewer(fCustomTree.getControl());
        fViewer.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
        fViewer.setUseHashlookup(true);
        fViewer.getControl().setData(ApplicationWorkbenchWindowAdvisor.FOCUSLESS_SCROLL_HOOK, new Object());
        fViewer.getControl().setFont(OwlUI.getThemeFont(OwlUI.HEADLINES_FONT_ID, SWT.NORMAL));

        /* Custom Tooltip for Feed Column */
        FeedColumnToolTipSupport.enableFor(fViewer);

        /* This is a Workaround until we remember expanded Groups */
        fViewer.setAutoExpandLevel(AbstractTreeViewer.ALL_LEVELS);

        fHandCursor = parent.getDisplay().getSystemCursor(SWT.CURSOR_HAND);

        /* Drag and Drop */
        initDragAndDrop();
    }

    /**
     * return the last selection in the table viewer.
     */
    IStructuredSelection getLastSelection() {
        return fLastSelection;
    }

    /**
     * return the last non empty selection in the table viewer.
     */
    IStructuredSelection getLastNonEmptySelection() {
        return fLastNonEmptySelection;
    }

    private void initDragAndDrop() {
        int ops = DND.DROP_COPY | DND.DROP_MOVE;
        Transfer[] transfers = new Transfer[] { LocalSelectionTransfer.getTransfer(), TextTransfer.getInstance(),
                URLTransfer.getInstance() };

        /* Drag Support */
        fViewer.addDragSupport(ops, transfers, new DragSourceListener() {
            public void dragStart(final DragSourceEvent event) {
                SafeRunner.run(new LoggingSafeRunnable() {
                    public void run() throws Exception {
                        LocalSelectionTransfer.getTransfer().setSelection(fViewer.getSelection());
                        LocalSelectionTransfer.getTransfer().setSelectionSetTime(event.time & 0xFFFFFFFFL);
                        event.doit = true;
                    }
                });
            }

            public void dragSetData(final DragSourceEvent event) {
                SafeRunner.run(new LoggingSafeRunnable() {
                    public void run() throws Exception {

                        /* Set Selection using LocalSelectionTransfer */
                        if (LocalSelectionTransfer.getTransfer().isSupportedType(event.dataType))
                            event.data = LocalSelectionTransfer.getTransfer().getSelection();

                        /* Set Text using Text- or URLTransfer */
                        else if (TextTransfer.getInstance().isSupportedType(event.dataType)
                                || URLTransfer.getInstance().isSupportedType(event.dataType))
                            setTextData(event);
                    }
                });
            }

            public void dragFinished(DragSourceEvent event) {
                SafeRunner.run(new LoggingSafeRunnable() {
                    public void run() throws Exception {
                        LocalSelectionTransfer.getTransfer().setSelection(null);
                        LocalSelectionTransfer.getTransfer().setSelectionSetTime(0);
                    }
                });
            }
        });
    }

    private void setTextData(DragSourceEvent event) {
        IStructuredSelection selection = (IStructuredSelection) LocalSelectionTransfer.getTransfer().getSelection();
        Collection<INews> news = ModelUtils.normalize(selection.toList());

        if (!news.isEmpty()) {
            StringBuilder strB = new StringBuilder();

            for (INews item : news) {
                String link = CoreUtils.getLink(item);
                if (StringUtils.isSet(link)) {
                    strB.append(link);

                    if (news.size() > 1)
                        strB.append("\n"); //$NON-NLS-1$
                }
            }

            if (strB.length() > 0)
                event.data = strB.toString();
        }
    }

    /*
     * @see org.rssowl.ui.internal.editors.feed.IFeedViewPart#getViewer()
     */
    public NewsTableViewer getViewer() {
        return fViewer;
    }

    /*
     * @see org.rssowl.ui.internal.editors.feed.IFeedViewPart#initViewer(org.eclipse.jface.viewers.IStructuredContentProvider,
     * org.eclipse.jface.viewers.ViewerFilter)
     */
    public void initViewer(IStructuredContentProvider contentProvider, ViewerFilter filter) {

        /* Apply ContentProvider */
        fViewer.setContentProvider(contentProvider);

        /* Create LabelProvider (Custom Owner Drawn enabled!) */
        NewsColumnViewModel columnModel = createColumnModel(fEditorInput.getMark());
        fNewsTableLabelProvider = new NewsTableLabelProvider(columnModel);
        if (!OwlUI.isHighContrast()) {
            fViewer.getControl().addListener(SWT.EraseItem, new Listener() {
                public void handleEvent(Event event) {
                    Object element = event.item.getData();
                    fNewsTableLabelProvider.erase(event, element);
                }
            });
        }

        /* Create Sorter */
        fNewsSorter = new NewsComparator();
        fViewer.setComparator(fNewsSorter);

        /* Set Comparer */
        fViewer.setComparer(getComparer());

        /* Create and Show Columns */
        fCustomTree.setFlat(!isGroupingEnabled());
        showColumns(columnModel, false, false);

        /* Add Filter */
        fViewer.addFilter(filter);

        /* Hook Contextual Menu */
        hookContextualMenu();

        /* Register Listeners */
        registerListeners();

        /* Propagate Selection Events */
        fFeedViewSite.getEditorSite().setSelectionProvider(fViewer);
    }

    /* Called when column settings have been updated */
    void updateColumns(Object input) {
        showColumns(createColumnModel(input), true, true);
    }

    private NewsColumnViewModel createColumnModel(Object input) {
        NewsColumnViewModel model;
        if (input instanceof IFolderChild)
            model = NewsColumnViewModel.loadFrom(Owl.getPreferenceService().getEntityScope((IEntity) input));
        else
            model = NewsColumnViewModel.createGlobal();

        /* Synthetically add the "Feed" column if both "Feed" and "Location" not present and if not grouping by feed */
        if ((input instanceof ISearchMark) || (input instanceof INewsBin) || (input instanceof FolderNewsMark)) {
            if (!model.getColumns().contains(NewsColumn.FEED)
                    && !model.getColumns().contains(NewsColumn.LOCATION)) {
                if (!isGroupingByFeed())
                    model.getColumns().add(1, NewsColumn.FEED);
            }
        }

        return model;
    }

    private boolean isGroupingEnabled() {
        IContentProvider contentProvider = fViewer.getContentProvider();
        if (contentProvider != null && contentProvider instanceof NewsContentProvider)
            return ((NewsContentProvider) contentProvider).isGroupingEnabled();

        return false;
    }

    private boolean isGroupingByFeed() {
        IContentProvider contentProvider = fViewer.getContentProvider();
        if (contentProvider != null && contentProvider instanceof NewsContentProvider)
            return ((NewsContentProvider) contentProvider).isGroupingByFeed();

        return false;
    }

    private boolean isGroupingByStickyness() {
        IContentProvider contentProvider = fViewer.getContentProvider();
        if (contentProvider != null && contentProvider instanceof NewsContentProvider)
            return ((NewsContentProvider) contentProvider).isGroupingByStickyness();

        return false;
    }

    private void showColumns(NewsColumnViewModel newModel, boolean update, boolean refresh) {
        if (fCustomTree.getControl().isDisposed())
            return;

        /* Return early if no change is required */
        if (newModel.equals(fColumnModel))
            return;

        /* Dispose Old */
        fBlockColumMoveEvent = true;
        try {
            fCustomTree.clear();
        } finally {
            fBlockColumMoveEvent = false;
        }

        /* Keep as current */
        fColumnModel = newModel;

        /* Create Columns */
        List<NewsColumn> cols = newModel.getColumns();
        for (int i = 0; i < cols.size(); i++) {
            NewsColumn col = cols.get(i);
            TreeViewerColumn viewerColumn = new TreeViewerColumn(fViewer, SWT.LEFT);
            fCustomTree.manageColumn(viewerColumn.getColumn(), newModel.getLayoutData(col),
                    col.showName() ? col.getName() : null, col.showTooltip() ? col.getName() : null, null,
                    col.isMoveable(), col.isResizable());
            if (i == 0)
                viewerColumn.getColumn().setResizable(true); //Need to override this due to bug on windows
            viewerColumn.getColumn().setData(NewsColumnViewModel.COL_ID, col);

            if (newModel.getSortColumn() == col && col.showSortIndicator()) {
                fCustomTree.getControl().setSortColumn(viewerColumn.getColumn());
                fCustomTree.getControl().setSortDirection(newModel.isAscending() ? SWT.UP : SWT.DOWN);
            }
        }

        /* Remember Column Order */
        fOldColumnOrder = fCustomTree.getControl().getColumnOrder();

        /* Update Tree */
        if (update)
            fCustomTree.update();

        /* Update Sorter */
        fNewsSorter.setAscending(newModel.isAscending());
        fNewsSorter.setSortBy(newModel.getSortColumn());

        /* Set Label Provider */
        fNewsTableLabelProvider.init(newModel);
        fViewer.setLabelProvider(fNewsTableLabelProvider);

        /* Refresh if necessary */
        if (refresh)
            fViewer.refresh(true);

        /* Enable Sorting adding listeners to Columns */
        TreeColumn[] columns = fCustomTree.getControl().getColumns();
        for (final TreeColumn column : columns) {
            column.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    NewsColumn oldSortBy = fNewsSorter.getSortBy();
                    NewsColumn newSortBy = (NewsColumn) column.getData(NewsColumnViewModel.COL_ID);
                    boolean defaultAscending = newSortBy.prefersAscending();
                    boolean ascending = (oldSortBy != newSortBy) ? defaultAscending : !fNewsSorter.isAscending();

                    /* Update Model */
                    fColumnModel.setSortColumn(newSortBy);
                    fColumnModel.setAscending(ascending);

                    /* Update Sorter */
                    fNewsSorter.setSortBy(newSortBy);
                    fNewsSorter.setAscending(ascending);

                    /* Indicate Sort-Column in UI for Columns that have a certain width */
                    if (newSortBy.showSortIndicator()) {
                        fCustomTree.getControl().setSortColumn(column);
                        fCustomTree.getControl().setSortDirection(ascending ? SWT.UP : SWT.DOWN);
                    } else {
                        fCustomTree.getControl().setSortColumn(null);
                    }

                    /* Refresh UI */
                    fViewer.refresh(false);

                    /* Save Column Model in Background */
                    saveColumnModelInBackground();
                }
            });

            /* Listen to moved columns */
            column.addListener(SWT.Move, new Listener() {
                public void handleEvent(Event event) {
                    if (fCustomTree.getControl().isDisposed() || fBlockColumMoveEvent)
                        return;

                    int[] columnOrder = fCustomTree.getControl().getColumnOrder();
                    if (!Arrays.equals(fOldColumnOrder, columnOrder)) {

                        /* Remember Old */
                        fOldColumnOrder = columnOrder;

                        /* Create Column Model from Control */
                        NewsColumnViewModel currentModel = NewsColumnViewModel
                                .initializeFrom(fCustomTree.getControl());
                        currentModel.setSortColumn(fNewsSorter.getSortBy());
                        currentModel.setAscending(fNewsSorter.isAscending());

                        /* Save in case the model changed */
                        if (!currentModel.equals(fColumnModel)) {
                            fColumnModel = currentModel;
                            saveColumnModelInBackground();
                        }
                    }
                }
            });
        }
    }

    private void registerListeners() {

        /* Open selected News Links in Browser on doubleclick */
        fViewer.addDoubleClickListener(new IDoubleClickListener() {
            public void doubleClick(DoubleClickEvent event) {
                onMouseDoubleClick(event);
            }
        });

        /* Hook into Statusline */
        fViewer.addSelectionChangedListener(
                new StatusLineUpdater(fFeedViewSite.getEditorSite().getActionBars().getStatusLineManager()));

        /* Track Selections in the Viewer */
        fSelectionChangeListener = new ISelectionChangedListener() {
            public void selectionChanged(SelectionChangedEvent event) {
                onSelectionChanged(event);
            }
        };
        fViewer.addSelectionChangedListener(fSelectionChangeListener);

        /* Perform Action on Mouse-Down */
        fCustomTree.getControl().addListener(SWT.MouseDown, new Listener() {
            public void handleEvent(Event event) {
                onMouseDown(event);
            }
        });

        /* Perform Action on Mouse-Up */
        fCustomTree.getControl().addListener(SWT.MouseUp, new Listener() {
            public void handleEvent(Event event) {
                onMouseUp(event);
            }
        });

        /* Update Cursor on Mouse-Move */
        fCustomTree.getControl().addListener(SWT.MouseMove, new Listener() {
            public void handleEvent(Event event) {
                onMouseMove(event);
            }
        });

        /* Redraw on Label update */
        fLabelListener = new LabelAdapter() {
            @Override
            public void entitiesUpdated(Set<LabelEvent> events) {
                JobRunner.runInUIThread(fViewer.getTree(), new Runnable() {
                    public void run() {
                        fViewer.refresh(true);
                    }
                });
            }
        };
        DynamicDAO.addEntityListener(ILabel.class, fLabelListener);

        /* Refresh Viewer when Sticky Color Changes */
        fPropertyChangeListener = new IPropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent event) {
                if (fViewer.getControl().isDisposed())
                    return;

                if (OwlUI.STICKY_BG_COLOR_ID.equals(event.getProperty())
                        || OwlUI.NEWS_LIST_BG_COLOR_ID.equals(event.getProperty())) {
                    ((NewsTableLabelProvider) fViewer.getLabelProvider()).updateResources();
                    fViewer.refresh(true);
                    fViewer.getTree().redraw();
                }
            }
        };
        PlatformUI.getWorkbench().getThemeManager().addPropertyChangeListener(fPropertyChangeListener);
    }

    private void onMouseDoubleClick(DoubleClickEvent event) {
        IStructuredSelection selection = (IStructuredSelection) event.getSelection();
        if (selection.isEmpty())
            return;

        Object firstElem = selection.getFirstElement();

        /* Open News */
        if (firstElem instanceof INews) {

            /* Do nothing if the user recently invokved a column action */
            if (System.currentTimeMillis() - fLastColumnActionInvokedMillies > 200)
                new OpenInBrowserAction(selection,
                        WebBrowserContext.createFrom((INews) firstElem, fEditorInput.getMark())).run();
        }

        /* Toggle expanded State of Group */
        else if (firstElem instanceof EntityGroup)
            fViewer.setExpandedState(firstElem, !fViewer.getExpandedState(firstElem));
    }

    private void onSelectionChanged(SelectionChangedEvent event) {

        /* Only consider Structured Selections */
        if (!(event.getSelection() instanceof IStructuredSelection))
            return;

        /* Remember */
        fLastSelection = (IStructuredSelection) event.getSelection();
        if (!fLastSelection.isEmpty())
            fLastNonEmptySelection = fLastSelection;

        /* Check Flag */
        if (fBlockNewsStateTracker.get())
            return;

        /* Retrieve all NewsReferences of the Selection */
        IStructuredSelection selection = (IStructuredSelection) event.getSelection();

        /* Only responsible for single Selection of a News */
        if (selection.size() != 1 || !(selection.getFirstElement() instanceof INews)) {
            fNewsStateTracker.cancel();
            fInstantMarkUnreadTracker.cancel();
            return;
        }

        /* Trigger the Tracker if news is not read already */
        final INews selectedNews = (INews) selection.getFirstElement();
        if (selectedNews.getState() != INews.State.READ && selectedNews.isVisible()) {
            final boolean markRead = fInputPreferences.getBoolean(DefaultPreferences.MARK_READ_STATE);
            final int delay = fNewsStateTracker.getDelay();

            /* Instantly mark as *unread* if required */
            if ((!markRead || delay > 0) && selectedNews.getState() != INews.State.UNREAD) {
                fInstantMarkUnreadTracker.run(new TaskAdapter() {
                    public IStatus run(IProgressMonitor monitor) {
                        setNewsState(selectedNews, INews.State.UNREAD, true);
                        return Status.OK_STATUS;
                    }
                });
            }

            /* Instantly Mark Read (see Bug 1023) */
            if (markRead && delay == 0)
                setNewsState(selectedNews, INews.State.READ, false);

            /* Mark Read after Delay */
            else if (markRead) {
                fNewsStateTracker.run(new TaskAdapter() {
                    public IStatus run(IProgressMonitor monitor) {
                        setNewsState(selectedNews, INews.State.READ, true);
                        return Status.OK_STATUS;
                    }
                });
            }
        }

        /* Cancel any possible running JobTracker */
        else if (selectedNews.getState() == INews.State.READ) {
            fNewsStateTracker.cancel();
            fInstantMarkUnreadTracker.cancel();
        }
    }

    private void onMouseUp(Event event) {

        /* Middle Mouse Button pressed */
        if (event.button == 2) {
            Point p = new Point(event.x, event.y);
            TreeItem item = fCustomTree.getControl().getItem(p);

            /* Problem - return */
            if (item == null || item.isDisposed())
                return;

            /* Open News */
            Object element = item.getData();
            if (element instanceof INews)
                new OpenInBrowserAction(new StructuredSelection(element),
                        WebBrowserContext.createFrom((INews) element, fEditorInput.getMark())).run();
        }
    }

    private void onMouseDown(Event event) {
        Point p = new Point(event.x, event.y);
        TreeItem item = fCustomTree.getControl().getItem(p);

        /* Problem - return */
        if (item == null || item.isDisposed())
            return;

        /* Mouse-Up over Read-State-Column */
        if (event.button == 1 && isInImageBounds(item, NewsColumn.TITLE, p)) {
            Object data = item.getData();

            /* Toggle State between Read / Unread */
            if (data instanceof INews) {
                INews news = (INews) data;
                INews.State newState = (news.getState() == INews.State.READ) ? INews.State.UNREAD
                        : INews.State.READ;

                /* Set State */
                fBlockNewsStateTracker.set(true);
                try {
                    setNewsState(news, newState, false);
                } finally {
                    fBlockNewsStateTracker.set(false);
                }
                fLastColumnActionInvokedMillies = System.currentTimeMillis();

                /*
                 * This is a workaround: Immediately after the mouse-down-event has been
                 * issued, a selection-event is triggered. This event is resulting in the
                 * news-state-tracker to run and mark the selected news as read again. To
                 * avoid this, we disable the tracker for a short while and set it back to
                 * enabled again.
                 */
                JobRunner.runDelayedFlagInversion(200, fBlockNewsStateTracker);
            }
        }

        /* Mouse-Up over Sticky-State-Column */
        else if (event.button == 1 && isInImageBounds(item, NewsColumn.STICKY, p)) {
            final Object data = item.getData();

            /* Toggle State between Sticky / Not Sticky */
            if (data instanceof INews) {
                Runnable runnable = new Runnable() {
                    public void run() {
                        new MakeNewsStickyAction(new StructuredSelection(data)).run();
                    }
                };

                INews news = (INews) data;
                if (news.getState() != INews.State.READ && isGroupingByStickyness()) //Workaround for Bug 1279
                    JobRunner.runInBackgroundThread(50, runnable);
                else
                    runnable.run();

                fLastColumnActionInvokedMillies = System.currentTimeMillis();
            }
        }

        /* Mouse-Up over Attachments-Column */
        else if (event.button == 1 && isInImageBounds(item, NewsColumn.ATTACHMENTS, p)) {
            Object data = item.getData();

            MenuManager contextMenu = new MenuManager();
            ApplicationActionBarAdvisor.fillAttachmentsMenu(contextMenu, new StructuredSelection(data),
                    fFeedViewSite.getEditorSite(), true);

            if (fAttachmentsMenu != null)
                OwlUI.safeDispose(fAttachmentsMenu);

            fAttachmentsMenu = contextMenu.createContextMenu(fViewer.getControl());

            Point cursorLocation = item.getDisplay().getCursorLocation();
            cursorLocation.y = cursorLocation.y + 16;
            fAttachmentsMenu.setLocation(cursorLocation);
            fAttachmentsMenu.setVisible(true);

            fLastColumnActionInvokedMillies = System.currentTimeMillis();
        }
    }

    private void onMouseMove(Event event) {
        Point p = new Point(event.x, event.y);
        TreeItem item = fCustomTree.getControl().getItem(p);

        /* Problem / Group hovered - reset */
        if (item == null || item.isDisposed() || item.getData() instanceof EntityGroup) {
            if (fShowsHandCursor && !fCustomTree.getControl().isDisposed()) {
                fCustomTree.getControl().setCursor(null);
                fShowsHandCursor = false;
            }
            return;
        }

        /* Show Hand-Cursor if action can be performed */
        boolean changeToHandCursor = isInImageBounds(item, NewsColumn.TITLE, p)
                || isInImageBounds(item, NewsColumn.STICKY, p) || isInImageBounds(item, NewsColumn.ATTACHMENTS, p);
        if (!fShowsHandCursor && changeToHandCursor) {
            fCustomTree.getControl().setCursor(fHandCursor);
            fShowsHandCursor = true;
        } else if (fShowsHandCursor && !changeToHandCursor) {
            fCustomTree.getControl().setCursor(null);
            fShowsHandCursor = false;
        }
    }

    /*
     * This Comparer is used to optimize some operations on the Viewer being used.
     * When deleting Entities, the Delete-Event is providing a reference to the
     * deleted Entity, which can not be resolved anymore. This Comparer will
     * return <code>TRUE</code> for a reference compared with an Entity that has
     * the same ID and is belonging to the same Entity. At any time, it _must_ be
     * avoided to call add, update or refresh with passing in a Reference!
     */
    private IElementComparer getComparer() {
        return new IElementComparer() {
            public boolean equals(Object a, Object b) {

                /* Quickyly check this common case */
                if (a == b)
                    return true;

                if (a instanceof ModelReference && b instanceof IEntity)
                    return ((ModelReference) a).references((IEntity) b);

                if (b instanceof ModelReference && a instanceof IEntity)
                    return ((ModelReference) b).references((IEntity) a);

                return a.equals(b);
            }

            public int hashCode(Object element) {
                return element.hashCode();
            }
        };
    }

    private void hookContextualMenu() {
        MenuManager manager = new MenuManager();
        manager.setRemoveAllWhenShown(true);
        manager.addMenuListener(new IMenuListener() {
            public void menuAboutToShow(IMenuManager manager) {
                final IStructuredSelection selection = (IStructuredSelection) fViewer.getSelection();
                boolean isEntityGroupSelected = ModelUtils.isEntityGroupSelected(selection);

                /* Open */
                if (!isEntityGroupSelected) {
                    manager.add(new Separator("open")); //$NON-NLS-1$

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

                /* Attachments */
                {
                    ApplicationActionBarAdvisor.fillAttachmentsMenu(manager, selection,
                            new SameShellProvider(fViewer.getTree().getShell()), false);
                }

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

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

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

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

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

                    /* Label */
                    ApplicationActionBarAdvisor.fillLabelMenu(manager, selection,
                            new SameShellProvider(fViewer.getTree().getShell()), false);
                }

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

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

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

                    Collections.sort(newsbins, comparator);

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

                    for (INewsBin bin : newsbins) {
                        if (fViewer.getInput() instanceof NewsBinReference
                                && bin.getId().equals(((NewsBinReference) fViewer.getInput()).getId()))
                            continue;

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

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

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

                    for (INewsBin bin : newsbins) {
                        if (fViewer.getInput() instanceof NewsBinReference
                                && bin.getId().equals(((NewsBinReference) fViewer.getInput()).getId()))
                            continue;

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

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

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

                /* Share */
                boolean entityGroupSelected = ModelUtils.isEntityGroupSelected(selection);
                if (!entityGroupSelected)
                    ApplicationActionBarAdvisor.fillShareMenu(manager, selection,
                            new SameShellProvider(fViewer.getTree().getShell()), false);

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

                /* Collapse Groups */
                if (entityGroupSelected) {
                    manager.add(new Separator());
                    ImageDescriptor icon = OwlUI.getImageDescriptor("icons/etool16/collapseall.gif");//$NON-NLS-1$
                    manager.add(new Action(Messages.NewsTableControl_COLLAPSE_GROUPS, icon) {
                        @Override
                        public void run() {
                            fViewer.getControl().setRedraw(false);
                            try {
                                fViewer.collapseAll();
                            } finally {
                                fViewer.getControl().setRedraw(true);
                            }
                        };
                    });
                }

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

                /* Show in Feed (only for searchmarks) */
                if (fViewer.getInput() instanceof SearchMarkReference && !selection.isEmpty()
                        && !isEntityGroupSelected) {
                    OpenNewsAction showInFeedAction = new OpenNewsAction(selection);
                    showInFeedAction.setText(Messages.NewsTableControl_SHOW_IN_FEED);
                    manager.appendToGroup("open", showInFeedAction); //$NON-NLS-1$
                }
            }
        });

        /* Create and Register with Workbench */
        Menu menu = manager.createContextMenu(fViewer.getControl());
        fViewer.getControl().setMenu(menu);
        fFeedViewSite.getEditorSite().registerContextMenu(manager, fViewer);
    }

    /*
     * @see org.rssowl.ui.internal.editors.feed.IFeedViewPart#setInput(java.lang.Object)
     */
    public void setPartInput(Object input) {

        /* Update Columns for Input */
        showColumns(createColumnModel(input), true, false);

        /* Set Input to Viewer */
        if (input instanceof IEntity)
            fViewer.setInput(((IEntity) input).toReference());
        else
            fViewer.setInput(input);
    }

    /**
     * Adjusts the scroll position to reflect the sorting.
     */
    public void adjustScrollPosition() {
        Tree tree = fViewer.getTree();
        int itemCount = tree.getItemCount();
        if (itemCount > 0) {
            if ((fNewsSorter.getSortBy() == NewsColumn.DATE || fNewsSorter.getSortBy() == NewsColumn.PUBLISHED
                    || fNewsSorter.getSortBy() == NewsColumn.MODIFIED
                    || fNewsSorter.getSortBy() == NewsColumn.RECEIVED) && fNewsSorter.isAscending()) {
                TreeItem item = tree.getItem(itemCount - 1);
                int childCount = item.getItemCount();
                if (childCount != 0)
                    item = item.getItem(childCount - 1);
                tree.showItem(item);
            } else
                tree.setTopItem(tree.getItem(0));
        }
    }

    /*
     * @see org.rssowl.ui.internal.editors.feed.IFeedViewPart#setFocus()
     */
    public void setFocus() {
        fViewer.getControl().setFocus();
    }

    /*
     * @see org.rssowl.ui.internal.editors.feed.IFeedViewPart#dispose()
     */
    public void dispose() {
        if (fAttachmentsMenu != null)
            OwlUI.safeDispose(fAttachmentsMenu);
        fNewsStateTracker.cancel();
        fInstantMarkUnreadTracker.cancel();
        fResources.dispose();
        unregisterListeners();
        fEditorInput = null;
    }

    void setBlockNewsStateTracker(boolean block) {
        fBlockNewsStateTracker.set(block);
    }

    private void unregisterListeners() {
        fViewer.removeSelectionChangedListener(fSelectionChangeListener);
        DynamicDAO.removeEntityListener(ILabel.class, fLabelListener);
        PlatformUI.getWorkbench().getThemeManager().removePropertyChangeListener(fPropertyChangeListener);
    }

    private void setNewsState(final INews news, final INews.State state, boolean async) {
        Runnable runnable = new Runnable() {
            public void run() {

                /* The news might have been marked as hidden/deleted meanwhile, so return */
                if (!news.isVisible())
                    return;

                Set<INews> singleNewsSet = Collections.singleton(news);
                boolean affectEquivalentNews = (state != INews.State.UNREAD && OwlUI.markReadDuplicates());

                /* Add to UndoStack */
                UndoStack.getInstance()
                        .addOperation(new NewsStateOperation(singleNewsSet, state, affectEquivalentNews));

                /* Perform Operation */
                fNewsDao.setState(singleNewsSet, state, affectEquivalentNews, false);
            }
        };

        if (async)
            JobRunner.runInUIThread(fViewer.getControl(), runnable);
        else
            runnable.run();
    }

    private int indexOf(NewsColumn column) {
        Tree tree = fCustomTree.getControl();
        if (tree.isDisposed())
            return -1;

        TreeColumn[] columns = tree.getColumns();
        for (int i = 0; i < columns.length; i++) {
            if (column == columns[i].getData(NewsColumnViewModel.COL_ID))
                return i;
        }

        return -1;
    }

    private boolean isInImageBounds(TreeItem item, NewsColumn column, Point p) {
        int index = indexOf(column);
        if (index == -1)
            return false;

        return item.getImageBounds(index).contains(p);
    }

    private void saveColumnModelInBackground() {
        final IPreferenceScope[] scope = new IPreferenceScope[1];
        final boolean[] saveMark = new boolean[] { false };
        final INewsMark mark = fEditorInput.getMark();

        IPreferenceScope entityPrefs = Owl.getPreferenceService().getEntityScope(mark);
        if (entityPrefs.hasKey(DefaultPreferences.BM_NEWS_COLUMNS)
                || entityPrefs.hasKey(DefaultPreferences.BM_NEWS_SORT_COLUMN)
                || entityPrefs.hasKey(DefaultPreferences.BM_NEWS_SORT_ASCENDING)) {
            scope[0] = entityPrefs; //Save to Entity
            saveMark[0] = true;
        } else
            scope[0] = fGlobalPreferences; //Save Globally

        final NewsColumnViewModel modelCopy = new NewsColumnViewModel(fColumnModel);
        JobRunner.runInBackgroundThread(new Runnable() {
            public void run() {
                modelCopy.saveTo(scope[0]);
                if (saveMark[0]) {
                    if (mark instanceof FolderNewsMark)
                        DynamicDAO.save(((FolderNewsMark) mark).getFolder());
                    else
                        DynamicDAO.save(mark);
                }
            }
        });
    }
}