Package com.salas.bb.views.mainframe

Source Code of com.salas.bb.views.mainframe.MainFrame

// BlogBridge -- RSS feed reader, manager, and web based service
// Copyright (C) 2002-2006 by R. Pito Salas
//
// This program is free software; you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software Foundation;
// either version 2 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with this program;
// if not, write to the Free Software Foundation, Inc., 59 Temple Place,
// Suite 330, Boston, MA 02111-1307 USA
//
// Contact: R. Pito Salas
// mailto:pitosalas@users.sourceforge.net
// More information: about BlogBridge
// http://www.blogbridge.com
// http://sourceforge.net/projects/blogbridge
//
// $Id: MainFrame.java,v 1.212 2008/04/15 10:30:32 spyromus Exp $
//

package com.salas.bb.views.mainframe;

import com.jgoodies.uif.AbstractFrame;
import com.jgoodies.uif.action.ActionManager;
import com.jgoodies.uif.application.Application;
import com.jgoodies.uif.application.ApplicationDescription;
import com.jgoodies.uif.component.UIFSplitPane;
import com.jgoodies.uif.util.SystemUtils;
import com.jgoodies.uif.util.WindowUtils;
import com.jgoodies.uifextras.util.PopupAdapter;
import com.jgoodies.uifextras.util.UIFactory;
import com.salas.bb.core.FeedRelocationController;
import com.salas.bb.core.GlobalController;
import com.salas.bb.core.GlobalModel;
import com.salas.bb.core.actions.*;
import com.salas.bb.core.actions.article.*;
import com.salas.bb.core.actions.feed.*;
import com.salas.bb.core.actions.guide.*;
import com.salas.bb.core.actions.logging.SwitchLogLevelAction;
import com.salas.bb.domain.IFeed;
import com.salas.bb.domain.NetworkFeed;
import com.salas.bb.domain.prefs.UserPreferences;
import com.salas.bb.imageblocker.BlockImageAction;
import com.salas.bb.remixfeeds.PostToBlogAction;
import com.salas.bb.search.*;
import com.salas.bb.service.ShowServiceDialogAction;
import com.salas.bb.service.sync.SyncInAction;
import com.salas.bb.service.sync.SyncOutAction;
import com.salas.bb.tags.ShowArticleTagsAction;
import com.salas.bb.tags.ShowFeedTagsAction;
import com.salas.bb.utils.ConnectionState;
import com.salas.bb.utils.Constants;
import com.salas.bb.utils.OSSettings;
import com.salas.bb.utils.dnd.DNDList;
import com.salas.bb.utils.dnd.DNDListContext;
import com.salas.bb.utils.i18n.Strings;
import com.salas.bb.utils.notification.NotificationArea;
import com.salas.bb.utils.osx.OSXSupport;
import com.salas.bb.utils.uif.UifUtilities;
import com.salas.bb.views.ActivityIndicatorView;
import com.salas.bb.views.ArticleListPanel;
import com.salas.bb.views.GuidesList;
import com.salas.bb.views.GuidesPanel;
import com.salas.bb.views.feeds.AbstractFeedDisplay;
import com.salas.bb.views.feeds.IFeedDisplay;
import com.salas.bb.views.feeds.html.ArticlesGroup;
import com.salas.bb.views.feeds.html.HTMLArticleDisplay;
import com.salas.bbnative.Taskbar;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.text.DefaultEditorKit;
import java.awt.*;
import java.awt.event.*;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;

/**
* Containing the main content of the application.
*/
public class MainFrame extends AbstractFrame
{
    private static final Logger LOG = Logger.getLogger(MainFrame.class.getName());

    /** default window size for small monitor */
    private static final Dimension WINDOW_SIZE_SMALL = new Dimension(620, 510);
    /** default window size for big monitor */
    private static final Dimension WINDOW_SIZE_BIG = new Dimension(760, 570);
    /** "big" monitors are at least this wide (after deducting taskbar insets) */
    private static final int WINDOW_WIDTH_BIG_THRESHOLD = 1200;
    /** minimum amount of window that must be initially on screen */
    private static final Dimension WINDOW_MIN_VISIBLE = new Dimension(80, 50);
    /** minimum size for window */
    private static final Dimension WINDOW_MIN_SIZE = new Dimension(300, 200);

    /** preferences key for storing left pane divider size */
    private static final String KEY_LEFT_SPLIT_DIV = "win.mainLeftSplitDiv";
    /** preferences key for storing rightpane divider size */
    private static final String KEY_RIGHT_SPLIT_DIV = "win.mainRightSplitDiv";

    private static final Border ACTIVITY_BORDER = new EmptyBorder(0, 0, 0, 10);
    private static final Border STATUS_BORDER = new EmptyBorder(0, 6, 0, 6);

    /** A feed that the feed link in the article title of a smart feed article belongs to. */
    public static IFeed         feedLinkFeed;

    private final ConnectionState connectionState;

    private ArticleListPanel    articleListPanel;
    private FeedsPanel          feedsPanel;
    private GuidesPanel         guidesPanel;

    private UIFSplitPane        rightSplitPane;
    private UIFSplitPane        leftSplitPane;

    private PopupAdapter        guidesListPopupAdapter;
    private PopupAdapter        feedListPopupAdapter;
    private PopupAdapter        feedLinkPopupAdapter;
    private PopupAdapter        htmlDisplayPopupAdapter;
    private PopupAdapter        imageDisplayPopupAdapter;
    private PopupAdapter        articleHyperLinkPopupAdapter;
    private PopupAdapter        articleGroupPopupAdapter;

    private JTextField          tfStatus;
    private JToolBar            toolbar;

    private Component           mainPane;
    private SearchField         searchField;
    private SearchPopupMenu     searchPopup;

    private boolean             minimizeToSystemTray;
    private JPanel statusBar;
    private Box iconsPanel;

    /** All popup menu types. */
    private static enum PopupMenuType
        { ARTICLE_HYPERLINK, FEEDS_LIST, GUIDES_LIST, HTML_ARTICLE, IMAGE_ARTICLE, ARTICLE_GROUP }

    private java.util.List<IPopupMenuHook> popupMenuHooks;

    /**
     * Build the actual main window with this call. This leads to the calls to the
     * other methods in this class.
     *
     * @param aConnectionState object for tracking the connection state.
     */
    public MainFrame(ConnectionState aConnectionState)
    {
        super("BlogBridge");
        connectionState = aConnectionState;
        popupMenuHooks = new ArrayList<IPopupMenuHook>();

        // TODO move to a better place
        UIManager.put("SearchPopupMenuItemUI", SearchPopupMenuItemUI.class.getName());
        UIManager.put("SearchPopupMenuItem.selectionForeground", Color.WHITE);
        UIManager.put("SearchPopupMenuItem.typeForeground", Color.GRAY);
        UIManager.put("SearchPopupMenuItem.typeSelectionForeground", Color.WHITE);

        searchField = new SearchField();
        searchPopup = new SearchPopupMenu();

        setFeedTitle(null);

        // Register a notification area listener which opens and focuses
        // the window upon the icon / message click.
        NotificationArea.setAppIconActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                restoreWindow();
            }
        });

        enableEvents(AWTEvent.WINDOW_FOCUS_EVENT_MASK | AWTEvent.WINDOW_STATE_EVENT_MASK);
    }

    private void restoreWindow()
    {
        int state = getExtendedState();

        // If window was previously iconified, restore its state and show taskbar button
        // if it was minimized to systray
        if ((state & ICONIFIED) != 0)
        {
            // If the window was iconified then we restore its button etc
            if (NotificationArea.isSupported() && minimizeToSystemTray)
            {
                showTaskbarButton();
            }

            setExtendedState((state & ~ICONIFIED));
        }
       
        toFront();
    }


    /**
     * Processes window state event occuring on this window by
     * dispatching them to any registered <code>WindowStateListener</code>
     * objects.
     *
     * @param e the window state event
     */
    protected void processWindowStateEvent(WindowEvent e)
    {
        boolean isIconified = (e.getNewState() & Frame.ICONIFIED) != 0;
        boolean wasIconified = (e.getOldState() & Frame.ICONIFIED) != 0;

        // Hide button if notification area is supported
        if (NotificationArea.isSupported() && minimizeToSystemTray &&
            !wasIconified && isIconified)
        {
            hideTaskbarButton();
        }
    }

    /**
     * Shows taskbar button.
     */
    private void showTaskbarButton()
    {
        setVisible(false);
        Taskbar.showButton(MainFrame.this);
        NotificationArea.setAppIconTempVisible(false);
        setVisible(true);
    }

    /**
     * Hides taskbar button.
     */
    private void hideTaskbarButton()
    {
        setVisible(false);
        NotificationArea.setAppIconTempVisible(true);
        Taskbar.hideButton(MainFrame.this);
    }

    /**
     * Changes the flag of this window minimization mode.
     *
     * @param flag <code>TRUE</code> to minimize the window to system tray (if supported).
     */
    public void setMinimizeToSystemTray(boolean flag)
    {
        minimizeToSystemTray = flag && OSSettings.isMinimizeToSystraySupported();
    }

    /**
     * Hides notifications when window gets focus.
     *
     * @param e event.
     */
    protected void processWindowFocusEvent(WindowEvent e)
    {
        if (e.getID() == WindowEvent.WINDOW_GAINED_FOCUS)
        {
            NotificationArea.setAppIconTempVisible(false);
        }
    }

    /**
     * Sets the title of window.
     *
     * @param feedTitle puts the name of selected feed into the title.
     */
    private void setFeedTitle(String feedTitle)
    {
        ApplicationDescription description = Application.getDescription();
        setTitle("BlogBridge - " +
            (feedTitle != null ? feedTitle + " - " : "") +
            description.getVersion());
    }

    /**
     * Returns search field component.
     *
     * @return search field.
     */
    public SearchField getSearchField()
    {
        return searchField;
    }

    /**
     * Register global application shortcuts.
     */
    public void registerShortcuts()
    {
        setupGlobalShortcuts();
        setupGuidesListShortcuts();
        setupFeedsListShortcuts();
        setupTestShortcuts();

        // Here we register additional global key events dispatcher
        // to track TAB / SHIFT-TAB.
        final KeyboardFocusManager keyFocusManager =
            KeyboardFocusManager.getCurrentKeyboardFocusManager();

        keyFocusManager.addKeyEventDispatcher(new FocusDispatcher());
    }

    private void setupTestShortcuts()
    {
        ActionMap ulAMap = getRootPane().getActionMap();
        ulAMap.put("test-f10", new AbstractAction()
        {
            public void actionPerformed(ActionEvent e)
            {
                System.out.println(AbstractLockupHandler.getThreadsDump(null));
            }
        });
        ulAMap.put("test-f11", new AbstractAction()
        {
            public void actionPerformed(ActionEvent e)
            {
            }
        });
        ulAMap.put("test-f12", new AbstractAction()
        {
            public void actionPerformed(ActionEvent e)
            {
            }
        });

        InputMap ulIMap = getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        ulIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F10,
            KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK),
            "test-f10");
        ulIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F11,
            KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK),
            "test-f11");
        ulIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F12,
            KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK),
            "test-f12");
    }

    /**
     * Registers global application shortcuts.
     * <ul>
     * <li><b>Ctrl-[1..9]</b> - switch to guide 1 - 9.</li>
     * <li><b>Ctrl-N</b> - subscribe to new feed.</li>
     * <li><b>Ctrl-M</b> - update channel.</li>
     * <li><b>Ctrl-Shift-M</b> - update guide.</li>
     * <li><b>Alt-Ctrl-Shift-M</b> - update all guides.</li>
     * <li><b>Ctrl-Up </b>/ <b>Ctrl-Down</b> - move selected guide up / down.</li>
     * <li><b>Space </b>/ <b>Shift-Space</b> - select next / previous unread article and
     *                                         mark current as read.</li>
     * <li><b>K</b> - select next article with keywords.</li>
     * <li><b>Ctrl-Shift-P </b> - show preferences dialog.</li>
     * <li><b>Ctrl-Shift-F10</b> - show Networking Manager.</li>
     * <li><b>Ctrl-"-"</b> - make article text font smaller.</li>
     * <li><b>Ctrl-"+"</b> - make article text font bigger.</li>
     * <li><b>Q </b>- toggle article read/unread.</li>
     * <li><b>Ctrl-Q </b>- mark article as read.</li>
     * <li><b>Ctrl-Shift-Q </b>- mark channel as read.</li>
     * <li><b>Ctrl-Up </b>/ <b>Ctrl-Down </b>- move selection upper / lower.</li>
     * <li><b>T</b> - show article tags.</li>
     * </ul>
     */
    private void setupGlobalShortcuts()
    {
        // Record the actions in the action Map
        ActionMap glAMap = getRootPane().getActionMap();
        glAMap.put("switchGuide", SwitchGuideAction.getInstance());
        glAMap.put("updateChannel", UpdateSelectedFeedsAction.getInstance());
        glAMap.put("updateGuide", UpdateGuideAction.getInstance());
        glAMap.put("updateAllGuides", UpdateAllGuidesAction.getInstance());
        glAMap.put("serviceDialog", ShowServiceDialogAction.getInstance());
        glAMap.put("syncIn", SyncInAction.getInstance());
        glAMap.put("syncOut", SyncOutAction.getInstance());
        glAMap.put("gotoNextUnreadArticle", GotoNextUnreadAction.getInstance());
        glAMap.put("gotoNextUnreadArticleInNextFeed", GotoNextUnreadInNextFeedAction.getInstance());
        glAMap.put("gotoPreviousUnreadArticle", GotoPreviousUnreadAction.getInstance());
        glAMap.put("showPreferences", ShowPreferencesAction.getInstance());
        glAMap.put("newChannel", AddDirectFeedAction.getInstance());
        glAMap.put("networkingManager", ShowActivityWindowAction.getInstance());
        glAMap.put("fontBiasUp", new FontSizeBiasChangeAction(1));
        glAMap.put("fontBiasDown", new FontSizeBiasChangeAction(-1));
        glAMap.put("networkingManager", ShowActivityWindowAction.getInstance());
        glAMap.put("search", SearchAction.getInstance());

        // Logging
        glAMap.put("logLevelAll", new SwitchLogLevelAction(Constants.EMPTY_STRING, Level.FINE));
        glAMap.put("logLevelBlogbridge", new SwitchLogLevelAction("com.salas.bb", Level.FINE));

        // Articles
        glAMap.put("markArticleAsRead", MarkArticleReadAction.getInstance());
        glAMap.put("toggleArticleRead", ToggleArticleReadAction.getInstance());
        glAMap.put("gotoNextArticle", GotoNextArticleAction.getInstance());
        glAMap.put("gotoPreviousArticle", GotoPreviousArticleAction.getInstance());
        glAMap.put("markChannelAsRead", MarkFeedAsReadAction.getInstance());
        glAMap.put("showArticleTags", ShowArticleTagsAction.getInstance());
        glAMap.put("copyArticleToClipboard", StyledTextCopyAction.getInstance());
        glAMap.put("postToBlogAlt", PostToBlogAction.getActionSelector());

        // Map those actions to specific keystrokes (inputMap)
        InputMap glIMap = getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        for (int i = KeyEvent.VK_1; i <= KeyEvent.VK_9; i++)
        {
            glIMap.put(KeyStroke.getKeyStroke(i, KeyEvent.CTRL_DOWN_MASK), "switchGuide");
        }

        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_M, KeyEvent.CTRL_DOWN_MASK),
            "updateChannel");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_M,
            KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK),
            "updateGuide");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_M,
            KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK | KeyEvent.ALT_DOWN_MASK),
            "updateAllGuides");

        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_P,
            KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK),
            "showPreferences");

        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F1, KeyEvent.CTRL_DOWN_MASK),
            "serviceDialog");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, KeyEvent.CTRL_DOWN_MASK),
            "syncIn");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F9, KeyEvent.CTRL_DOWN_MASK),
            "syncOut");

        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, ctrlOrCmd()), "fontBiasUp");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, ctrlOrCmd()), "fontBiasUp");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, ctrlOrCmd()), "fontBiasDown");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, ctrlOrCmd()), "fontBiasDown");

        // Logging
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_1,
            KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK),
            "logLevelAll");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_2,
            KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK),
            "logLevelBlogbridge");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_3,
            KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK),
            "logLevelInforma");

        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0),
            "gotoNextUnreadArticle");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, KeyEvent.CTRL_DOWN_MASK),
            "gotoNextUnreadArticleInNextFeed");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, KeyEvent.SHIFT_DOWN_MASK),
            "gotoPreviousUnreadArticle");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_K, 0),
            "gotoNextActicleWithKeywords");

        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_N, ctrlOrCmd()), "newChannel");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F10,
            KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK),
            "networkingManager");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F, ctrlOrCmd()), "search");

        // Articles
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Q, 0), "toggleArticleRead");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Q, KeyEvent.CTRL_DOWN_MASK),
            "markArticleAsRead");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, KeyEvent.CTRL_DOWN_MASK),
            "gotoNextArticle");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.CTRL_DOWN_MASK),
            "gotoPreviousArticle");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Q, KeyEvent.CTRL_DOWN_MASK |
            KeyEvent.SHIFT_DOWN_MASK), "markChannelAsRead");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_T, 0), "showArticleTags");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.CTRL_DOWN_MASK |
            KeyEvent.SHIFT_DOWN_MASK), "copyArticleToClipboard");
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_B, 0), "postToBlogAlt");

        // Text components compatibility shortcuts
        if (SystemUtils.IS_OS_WINDOWS)
        {
            addWindowsShortcuts("TextField");
            addWindowsShortcuts("TextArea");
            addWindowsShortcuts("TextPane");
            addWindowsShortcuts("EditorPane");
        }
    }

    /**
     * Adds Windows editor keys to the component input map.
     *
     * @param component component name.
     */
    private static void addWindowsShortcuts(String component)
    {
        InputMap inputMap = (InputMap)UIManager.getDefaults().get(component + ".focusInputMap");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, InputEvent.CTRL_MASK), DefaultEditorKit.copyAction);
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, InputEvent.SHIFT_MASK), DefaultEditorKit.cutAction);
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, InputEvent.SHIFT_MASK), DefaultEditorKit.pasteAction);
    }

    private static int ctrlOrCmd()
    {
        return SystemUtils.IS_OS_MAC ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK;
    }

    /**
     * Registers shortcuts for guides list.
     * <ul>
     * <li><b>Insert </b>- add new guide.</li>
     * <li><b>Delete </b>- delete guide.</li>
     * </ul>
     */
    private void setupGuidesListShortcuts()
    {
        ActionMap ulAMap = guidesPanel.getActionMap();
        ulAMap.put("deleteGuide", DeleteGuideAction.getInstance());
        ulAMap.put("newGuide", AddGuideAction.getInstance());

        InputMap ulIMap = guidesPanel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

        ulIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "deleteGuide");
        ulIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0), "newGuide");

        // Reseting guide list bindings
        GuidesList guidesList = guidesPanel.getGuidesList();
        InputMap glIMap = guidesList.getInputMap(JComboBox.WHEN_FOCUSED).getParent();
        if (glIMap != null)
        {
            glIMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0));
            glIMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, KeyEvent.CTRL_DOWN_MASK));
            glIMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, KeyEvent.SHIFT_DOWN_MASK));
            glIMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.CTRL_DOWN_MASK));
            glIMap.remove(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, KeyEvent.CTRL_DOWN_MASK));
        }

        // Add feed paste operation to the list
        ActionMap glAMap = guidesList.getActionMap();
        glIMap = guidesList.getInputMap(JComboBox.WHEN_FOCUSED);
        glAMap.put("pasteFeeds", FeedRelocationController.PASTE_OPERATION);
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, ctrlOrCmd()), "pasteFeeds");
        glAMap.put("abortCopyMoveFeeds", FeedRelocationController.ABORT_OPERATION);
        glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "abortCopyMoveFeeds");

        if (SystemUtils.IS_OS_WINDOWS)
        {
            glIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, KeyEvent.SHIFT_DOWN_MASK), "pasteFeeds");
        }
    }

    /**
     * Registers shortcuts for channels list.
     * <ul>
     * <li><b>Alt-Up </b>/ <b>Alt-Down </b>- move channel up / down in the list.</li>
     * <li><b>Insert </b>- new channel.</li>
     * <li><b>Delete </b>- delete channel.</li>
     * <li><b>Ctrl-Shift-Q </b>- mark channel as read.</li>
     * <li><b>Ctrl-P </b>- show feed properties dialog.</li>
     * <li><b>T</b> - show feed tags dialog.</li>
     * </ul>
     */
    private void setupFeedsListShortcuts()
    {
        ActionMap clAMap = feedsPanel.getActionMap();
        InputMap clIMap = feedsPanel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

        clAMap.put("moveChannelUp", new MoveSelectedFeedUpAction(feedsPanel.feedsList));
        clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.ALT_DOWN_MASK), "moveChannelUp");

        clAMap.put("moveChannelDown", new MoveSelectedFeedDownAction(feedsPanel.feedsList));
        clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, KeyEvent.ALT_DOWN_MASK), "moveChannelDown");

        clAMap.put("newChannel", AddDirectFeedAction.getInstance());
        clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0), "newChannel");

        clAMap.put("deleteChannel", DeleteFeedAction.getInstance());
        clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "deleteChannel");
        clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.SHIFT_DOWN_MASK), "deleteChannel");

        clAMap.put("markChannelAsRead", MarkFeedAsReadAction.getInstance());
        clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Q, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK),
                "markChannelAsRead");

        clAMap.put("showFeedProps", ShowFeedPropertiesAction.getInstance());
        clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_P, KeyEvent.CTRL_DOWN_MASK), "showFeedProps");

        clAMap.put("showFeedTags", ShowFeedTagsAction.getInstance());
        clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_T, 0), "showFeedTags");

        // Feed list map
        clAMap = feedsPanel.getFeedsList().getActionMap();
        clIMap = feedsPanel.getFeedsList().getInputMap(JComponent.WHEN_FOCUSED);

        clAMap.put("copyFeeds", FeedRelocationController.COPY_OPERATION);
        clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, ctrlOrCmd()), "copyFeeds");
        clAMap.put("cutFeeds", FeedRelocationController.CUT_OPERATION);
        clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, ctrlOrCmd()), "cutFeeds");
        clAMap.put("pasteFeeds", FeedRelocationController.PASTE_OPERATION);
        clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, ctrlOrCmd()), "pasteFeeds");
        clAMap.put("abortCopyMoveFeeds", FeedRelocationController.ABORT_OPERATION);
        clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "abortCopyMoveFeeds");
        if (SystemUtils.IS_OS_WINDOWS)
        {
            clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, KeyEvent.CTRL_DOWN_MASK), "copyFeeds");
            clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.SHIFT_DOWN_MASK), "cutFeeds");
            clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, KeyEvent.SHIFT_DOWN_MASK), "pasteFeeds");
        }

        clAMap.put("cycleViewModeForward", CycleViewModeForwardAction.getInstance());
        clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.CTRL_DOWN_MASK), "cycleViewModeForward");
        clAMap.put("cycleViewModeBackward", CycleViewModeBackwardAction.getInstance());
        clIMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, KeyEvent.CTRL_DOWN_MASK), "cycleViewModeBackward");
    }

    /**
     * Build and return content pane.
     *
     * @return pane.
     */
    protected JComponent buildContentPane()
    {
        initComponents();

        BBMenuBuilder menuBldr = new BBMenuBuilder();
        BBToolBarBuilder tbBldr = new BBToolBarBuilder();

        // Build and attach the menu bar
        JMenuBar myMenuBar = menuBldr.buildMenuBar();
        setJMenuBar(myMenuBar);

        UIFactory.createPlainLabel(Application.getDescription().getCopyright());

        // Create the Content Pane for the main frame, set it up to be a gridbag and then add
        // each subpanel with the appropriate GridBag Constraint. (Sketch this on paper first :-)

        JPanel topPanel = new JPanel();
        topPanel.setLayout(new BorderLayout());

        toolbar = tbBldr.buildToolBar();
        topPanel.add(toolbar, BorderLayout.NORTH);

        mainPane = buildMainPane();
        topPanel.add(mainPane, BorderLayout.CENTER);
        statusBar = buildStatusBar();
        topPanel.add(statusBar, BorderLayout.SOUTH);

        // Choose default window size
        // Note this will get overridden in restoreState if we have
        // any saved values from a prevous session.
        Rectangle visRect = getVisibleScreenRect(new Rectangle());
        Dimension prefSize = (visRect.width >= WINDOW_WIDTH_BIG_THRESHOLD)
            ? WINDOW_SIZE_BIG : WINDOW_SIZE_SMALL;

        topPanel.setPreferredSize(prefSize);

        Preferences prefs = Application.getUserPreferences();
        setToolbarVisible(prefs.getBoolean(UserPreferences.PROP_SHOW_TOOLBAR,
            UserPreferences.DEFAULT_SHOW_TOOLBAR));
        setToolbarLabelsVisible(prefs.getBoolean(UserPreferences.PROP_SHOW_TOOLBAR_LABELS,
            UserPreferences.DEFAULT_SHOW_TOOLBAR_LABELS));

        return topPanel;
    }

    /**
     * Adds an icon component to the status bar icon section.
     *
     * @param icon icon.
     */
    public void addIconComponent(JComponent icon)
    {
        icon.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5));
        iconsPanel.add(icon, 0);
        statusBar.validate();
    }

    /**
     * Changes the state of toolbar button labels.
     *
     * @param visible <code>TRUE</code> to make labels visible.
     */
    public void setToolbarLabelsVisible(boolean visible)
    {
        Component[] components = toolbar.getComponents();
        for (Component component : components)
        {
            if (component instanceof JButton)
            {
                JButton btn = (JButton)component;

                Action action = btn.getAction();
                if (action != null) btn.setText(visible ? (String)action.getValue("Name") : null);
            }
        }

        invalidate();
    }

    /**
     * Shows or hides toolbar.
     *
     * @param visible <code>TRUE</code> to show toolbar.
     */
    public void setToolbarVisible(boolean visible)
    {
        toolbar.setVisible(visible);
    }

    /**
     * Override to return our minimum window size. Unfortunately Swing doesn't
     * enforce this as the window border is being dragged; instead, the window
     * bounces back to this size when the border is released.
     */
    public Dimension getWindowMinimumSize()
    {
        return WINDOW_MIN_SIZE;
    }

    /**
     * Return a visible screen rectangle available for positioning a window.
     * Takes into account top-level insets like Windows taskbar, and
     * multi-monitor configurations
     *
     * @param findRect
     *            If multi-monitor configuration, use the monitor which on which
     *            this rectangle most covers. If findRect is an empty rectangle,
     *            this will use the default monitor
     *
     * @return Visible rectangle
     */
    protected Rectangle getVisibleScreenRect(Rectangle findRect)
    {
        GraphicsEnvironment grEnv = GraphicsEnvironment.getLocalGraphicsEnvironment();

        // Iterate over all monitors and find the one with the
        // greatest area of overlap with findRect.
        int maxOverlap = 0;
        GraphicsConfiguration grConfig = grEnv.getDefaultScreenDevice().getDefaultConfiguration();
        GraphicsDevice[] gs = grEnv.getScreenDevices();
        for (GraphicsDevice gd : gs)
        {
            Rectangle r = gd.getDefaultConfiguration().getBounds();
            Dimension overSize = r.intersection(findRect).getSize();
            int overlap = overSize.width * overSize.height;
            if (overlap > maxOverlap)
            {
                maxOverlap = overlap;
                grConfig = gd.getDefaultConfiguration();
            }
        }

        Rectangle screenBounds = grConfig.getBounds();

        // Reduce bounds by the insets which represent always-on-top
        // toolbars on edge of screen.
        Insets screenInsets = getToolkit().getScreenInsets(grConfig);

        // A bug in Java 1.4.1 causes negative insets sometimes
        // on multiple monitors (see Java community bug database #4654713)
        screenInsets.left = Math.abs(screenInsets.left);
        screenInsets.right = Math.abs(screenInsets.right);
        screenInsets.top = Math.abs(screenInsets.top);
        screenInsets.bottom = Math.abs(screenInsets.bottom);

        screenBounds.x += screenInsets.left;
        screenBounds.y += screenInsets.top;
        screenBounds.width -= (screenInsets.left + screenInsets.right);
        screenBounds.height -= (screenInsets.top + screenInsets.bottom);

        return screenBounds;
    }

    /**
     * Initializes components of the frame.
     */
    private void initComponents()
    {
        articleListPanel = new ArticleListPanel();
        feedsPanel = new FeedsPanel();
        guidesPanel = new GuidesPanel();
        guidesPanel.setupGuidesList(getGuidesPopupAdapter());

        // On double click actions setup
        feedsPanel.setOnDoubleClickAction(OpenBlogHomeAction.getInstance());
        guidesPanel.setOnDoubleClickAction(GuidePropertiesAction.getInstance());

        // Enable DND from Channel list to Guide list.
        feedsPanel.getFeedsList().addPropertyChangeListener(DNDList.PROP_MOUSE_DRAGGED_EVENT,
                guidesPanel.getGuidesList());

        // Search objects
        Dimension size = searchField.getPreferredSize();
        size.width = 100;
        searchField.setMinimumSize(size);
        searchField.setPreferredSize(size);
        searchField.setMaximumSize(size);
        searchPopup.setInvoker(this);

// TODO !!! review !!!
//        articleListPanel.getFeedView().addArticleSelectionListener(this);
    }

    /**
     * MainPane consists of a ChannelGuidePanel on the left and the SplitPane in the center.
     *
     * @return main pane.
     */
    private Component buildMainPane()
    {
        JPanel mainPane = new JPanel(new BorderLayout());

        buildRightSplitPane();
        buildLeftSplitPane();
        mainPane.add(leftSplitPane, BorderLayout.CENTER);

        return mainPane;
    }

    /**
     * This consist of the ChannelGuidePanel on the left and the RightSplitPanel on the right,
     * with a movable split bar.
     */
    private void buildLeftSplitPane()
    {
        leftSplitPane = (UIFSplitPane)
            UIFactory.createStrippedSplitPane(JSplitPane.HORIZONTAL_SPLIT, guidesPanel,
                rightSplitPane, 0);

        // On OS X the correct width of a split pane divider is a little thinner.
        if (SystemUtils.IS_OS_MAC)
        {
            leftSplitPane.setBorder(BorderFactory.createEmptyBorder(0, 7, 0, 7));
            leftSplitPane.setDividerSize(OSXSupport.SPLITPANE_WIDTH);
        } else
        {
            leftSplitPane.setBorder(BorderFactory.createEmptyBorder(7, 7, 0, 7));
        }
    }

    /**
     * This consist of the ChannelListPanel on the left and the ItemListPanel on the right,
     * with a movable split bar.
     */
    private void buildRightSplitPane()
    {
        rightSplitPane = (UIFSplitPane)
            UIFactory.createStrippedSplitPane(JSplitPane.HORIZONTAL_SPLIT, feedsPanel,
                articleListPanel, 0);

        // On OS X the correct width of a split pane divider is a little thinner.
        if (SystemUtils.IS_OS_MAC)
        {
            rightSplitPane.setDividerSize(OSXSupport.SPLITPANE_WIDTH);
        } else
        {
            rightSplitPane.setBorder(BorderFactory.createEmptyBorder());
        }
    }

    /**
     * Create and configure the StatusBar.
     *
     * @return status bar.
     */
    private JPanel buildStatusBar()
    {
        JPanel panel = new JPanel(new BorderLayout());

        // Display activity and connection indicators
        iconsPanel = Box.createHorizontalBox();
        iconsPanel.setBorder(ACTIVITY_BORDER);

        JComponent activityIndicator = new ActivityIndicatorView(connectionState,
                new MouseAdapter()
                {
                    public void mouseClicked(MouseEvent e)
                    {
                        ConnectionStateSwitchAction.getInstance().actionPerformed(null);
                    }
                });
        iconsPanel.add(activityIndicator);

        // On OS X, the rightmost position of the status bar is usurped by the silly little resize
        // control.
        if (SystemUtils.IS_OS_MAC)
        {
            final JPanel spacer = new JPanel();
            spacer.setPreferredSize(new Dimension(10, -1));
            iconsPanel.add(spacer);
        }

        panel.add(iconsPanel, BorderLayout.EAST);

        // Status bar
        tfStatus = new JTextField()
        {
            @Override
            protected void processEvent(AWTEvent e)
            {
                // No events in labels
            }

            @Override
            public void updateUI()
            {
                super.updateUI();
                setOpaque(false);
                setMargin(new Insets(0, 0, 0, 0));
            }
        };
        tfStatus.setFocusable(false);
        tfStatus.setBorder(STATUS_BORDER);
        tfStatus.setEditable(false);
        panel.add(tfStatus, BorderLayout.CENTER);

        return panel;
    }

    /**
     * Sets the status of the application or other information to the status bar.
     *
     * @param status
     *        status.
     */
    public void setStatus(String status)
    {
        tfStatus.setText(status);
        if (status != null) tfStatus.setCaretPosition(0);
    }

    /**
     * Called just before we close, for an opportunity to save state.
     *
     * JGoodies would normally call storeState from the ApplicationClosingHandler,
     * but our shutdown process quits before we get that far, so this is alternate wiring.
     */
    public void prepareToClose()
    {
        // The storeState call will save bound our bounds and window state.
        storeState();

        final Preferences prefs = Application.getUserPreferences();

        if (leftSplitPane != null && rightSplitPane != null)
        {
            prefs.putInt(KEY_LEFT_SPLIT_DIV, leftSplitPane.getDividerLocation());
            prefs.putInt(KEY_RIGHT_SPLIT_DIV, rightSplitPane.getDividerLocation());
        }
    }

    /**
     * Stores window state before closing.
     */
    protected void storeState()
    {
        Preferences userPrefs = Application.getUserPreferences();
        WindowUtils.storeBounds(userPrefs, this);
        WindowUtils.storeState(userPrefs, this);
    }

    /**
     * Restore our window state as we are being launched.
     *
     * Restore state after all is built.
     */
    protected void restoreState()
    {
        // Restore window position
        super.restoreState();

        // Restore divider sizes
        final Preferences prefs = Application.getUserPreferences();
        int leftSplitDiv = prefs.getInt(KEY_LEFT_SPLIT_DIV, -1);
        int rightSplitDiv = prefs.getInt(KEY_RIGHT_SPLIT_DIV, -1);

        // Do some simple sanity checking
        if (leftSplitDiv > 0 && rightSplitDiv > 0 &&
            (leftSplitDiv + rightSplitDiv) < getBounds().width)
        {
            leftSplitPane.setDividerLocation(leftSplitDiv);
            rightSplitPane.setDividerLocation(rightSplitDiv);
        }

        // Make sure window is not sized larger than the current screen, and that
        // at least the top-left corner is visible on the current screen, so that
        // it can be dragged to make it fully visible.
        Rectangle r = getBounds();

        Rectangle visRect = getVisibleScreenRect(r);
        r.x = Math.min(r.x, visRect.width - WINDOW_MIN_VISIBLE.width);
        r.y = Math.min(r.y, visRect.height - WINDOW_MIN_VISIBLE.height);
        r.x = Math.max(r.x, visRect.x);
        r.y = Math.max(r.y, visRect.y);
        r.width = Math.min(r.width, visRect.width);
        r.height = Math.min(r.height, visRect.height);

        setBounds(r);

        GlobalController.SINGLETON.selectFeed(GlobalModel.SINGLETON.getSelectedFeed());

        registerShortcuts();

        // Setup notification area menu
        if (NotificationArea.isSupported())
        {
            final String miShowBB = Strings.message("systray.menu.showbb");
            final String miCheckUpdates = Strings.message("systray.menu.checkupdates");
            final String miExit = Strings.message("systray.menu.exit");

            PopupMenu menu = new PopupMenu();
            menu.add(miShowBB);
            menu.addSeparator();
            menu.add(miCheckUpdates);
            menu.addSeparator();
            menu.add(miExit);

            NotificationArea.setAppIconMenu(menu);

            menu.addActionListener(new ActionListener()
            {
                public void actionPerformed(ActionEvent e)
                {
                    if (miShowBB.equals(e.getActionCommand()))
                    {
                        restoreWindow();
                    } else if (miCheckUpdates.equals(e.getActionCommand()))
                    {
                        ActionManager.get(ActionsTable.CMD_GUIDE_RELOAD_ALL_SM).actionPerformed(null);
                    } else if (miExit.equals(e.getActionCommand()))
                    {
                        ActionManager.get(ActionsTable.CMD_BB_EXIT).actionPerformed(null);
                    }
                }
            });
        }

        // Don't let the application start in iconified mode.
        setExtendedState(getExtendedState() & ~ICONIFIED);
    }

    /**
     * Returns channels list panel reference.
     *
     * @return list panel.
     */
    public FeedsPanel getFeedsPanel()
    {
        return feedsPanel;
    }

    /**
     * Returns reference onto guides panel.
     *
     * @return guides panel.
     */
    public GuidesPanel getGudiesPanel()
    {
        return guidesPanel;
    }

    /**
     * Returns items list panel reference.
     *
     * @return list panel.
     */
    public ArticleListPanel getArticlesListPanel()
    {
        return articleListPanel;
    }

    /**
     * Selects the feed in list.
     *
     * @param feed
     *        feed to select.
     */
    public void selectFeed(IFeed feed)
    {
        updateTitle(feed);

        if (feedsPanel != null) feedsPanel.selectListItem(feed);
    }

    /**
     * Updates title of the frame with name of currently selected feed.
     *
     * @param feed feed to select.
     */
    public void updateTitle(IFeed feed)
    {
        setFeedTitle(feed == null ? null : feed.getTitle());
    }

    /**
     * Returns a popup adapter for the guides list.
     *
     * @return popup adapter.
     */
    public synchronized PopupAdapter getGuidesPopupAdapter()
    {
        if (guidesListPopupAdapter == null)
        {
            guidesListPopupAdapter = new PopupAdapter()
            {
                protected JPopupMenu buildPopupMenu(MouseEvent anevent)
                {
                    JPopupMenu menu = new NonlockingPopupMenu(Strings.message("ui.popup.feeds"));

                    menu.add(ActionManager.get(ActionsTable.CMD_GUIDE_RELOAD));
                    menu.addSeparator();

                    menu.add(ActionManager.get(ActionsTable.CMD_GUIDE_ADD));
                    menu.add(ActionManager.get(ActionsTable.CMD_GUIDE_DELETE));
                    menu.add(ActionManager.get(ActionsTable.CMD_GUIDE_MERGE));
                    menu.add(ActionManager.get(ActionsTable.CMD_GUIDE_SUBSCRIBE_READINGLIST));
                    menu.add(ActionManager.get(ActionsTable.CMD_GUIDE_POST_TO_BLOG));
                    menu.addSeparator();

                    menu.add(ActionManager.get(ActionsTable.CMD_GUIDE_MARK_READ));
                    menu.add(ActionManager.get(ActionsTable.CMD_GUIDE_MARK_UNREAD));
                    menu.addSeparator();

                    menu.add(ActionManager.get(ActionsTable.CMD_GUIDE_IMPORT));
                    menu.add(ActionManager.get(ActionsTable.CMD_GUIDE_EXPORT));

                    menu.addSeparator();
                    menu.add(ActionManager.get(ActionsTable.CMD_GUIDE_SORT_BY_TITLE));
                    menu.add(ActionManager.get(ActionsTable.CMD_GUIDE_PROPERTIES));

                    appendCustomActions(menu, PopupMenuType.GUIDES_LIST);

                    return menu;
                }
            };
        }

        return guidesListPopupAdapter;
    }

    /**
     * Returns a PopupAdapter for the Feeds List right click menu.
     *
     * @return popup adapter.
     */
    public synchronized PopupAdapter getFeedsListPopupAdapter()
    {
        if (feedListPopupAdapter == null)
        {
            feedListPopupAdapter = new PopupAdapter()
            {

                protected JPopupMenu buildPopupMenu(MouseEvent anevent)
                {
                    JPopupMenu menu = new NonlockingPopupMenu(Strings.message("ui.popup.feeds"));

                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_RELOAD));
                    menu.addSeparator();

                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_MARK_READ));
                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_MARK_UNREAD));
                    menu.addSeparator();

                    // Make sure the icon is never displayed.
                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_SUBSCRIBE));
                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_ADD_SMART_FEED));

                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_DELETE));
                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_PROPERTIES));
                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_TAGS));
                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_POST_TO_BLOG));
                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_DISCOVER));

                    menu.addSeparator();
                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_BROWSE));

                    appendCustomActions(menu, PopupMenuType.FEEDS_LIST);

                    return menu;
                }
            };
        }

        return feedListPopupAdapter;
    }

    /**
     * Returns a PopupAdapter for the Feeds Link in the article header (Smart Feed).
     *
     * @return popup adapter.
     */
    public synchronized PopupAdapter getFeedLinkPopupAdapter()
    {
        if (feedLinkPopupAdapter == null)
        {
            feedLinkPopupAdapter = new PopupAdapter()
            {
                protected JPopupMenu buildPopupMenu(MouseEvent anevent)
                {
                    JPopupMenu menu = new NonlockingPopupMenu(Strings.message("ui.popup.feeds"));

                    FeedLinkMarkFeedAsReadAction.setFeed(feedLinkFeed);
                    FeedLinkMarkFeedAsUnreadAction.setFeed(feedLinkFeed);
                    FeedLinkShowFeedPropertiesAction.setFeed(feedLinkFeed);
                    FeedLinkShowFeedTagsAction.setFeed(feedLinkFeed);
                    FeedLinkPostToBlogAction.setFeed(feedLinkFeed);

                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_LINK_MARK_READ));
                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_LINK_MARK_UNREAD));

                    menu.addSeparator();
                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_LINK_PROPERTIES));
                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_LINK_TAGS));
                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_LINK_POST_TO_BLOG));
                    menu.add(ActionManager.get(ActionsTable.CMD_FEED_DISCOVER));

                    return menu;
                }
            };
        }

        return feedLinkPopupAdapter;
    }

    /**
     * Returns a popup adapter for HTML feed display.
     *
     * @return popup adapter.
     */
    public synchronized PopupAdapter getHTMLDisplayPopupAdapter()
    {
        if (htmlDisplayPopupAdapter == null)
        {
            htmlDisplayPopupAdapter = new PopupAdapter()
            {
                protected JPopupMenu buildPopupMenu(MouseEvent anevent)
                {
                    boolean canHaveSelection = false;
                    if (anevent.getSource() instanceof AbstractFeedDisplay)
                    {
                        AbstractFeedDisplay fd = (AbstractFeedDisplay)anevent.getSource();
                        SelectedTextCopyAction.setDisplay(fd);
                        canHaveSelection = true;
                    }
                   
                    JPopupMenu menu = new NonlockingPopupMenu(Strings.message("ui.popup.articles"))
                    {
                        /**
                         * Sets the visibility of the popup menu.
                         *
                         * @param visible true to make the popup visible, or false to
                         *          hide it
                         */
                        public void setVisible(boolean visible)
                        {
                            super.setVisible(visible);
                            if (!visible)
                            {
                                SelectedTextCopyAction.setDisplay(null);
                            }
                        }
                    };

                    addBlockImageCommand(menu);

                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_MARK_READ));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_MARK_UNREAD));

                    menu.addSeparator();
                   
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_BROWSE));
                    if (canHaveSelection) menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_COPY_TEXT));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_COPY_LINK));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_SEND_LINK));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_POST_TO_BLOG));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_TWEET_THIS));

                    menu.addSeparator();

                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_PROPERTIES));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_TAGS));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_PIN_UNPIN));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_DISCOVER));

                    appendCustomActions(menu, PopupMenuType.HTML_ARTICLE);

                    return menu;
                }
            };
        }

        return htmlDisplayPopupAdapter;
    }

    /**
     * Returns a popup adapter for image feed display.
     *
     * @return popup adapter.
     */
    public synchronized PopupAdapter getImageDisplayPopupAdapter()
    {
        if (imageDisplayPopupAdapter == null)
        {
            imageDisplayPopupAdapter = new PopupAdapter()
            {
                protected JPopupMenu buildPopupMenu(MouseEvent anevent)
                {
                    JPopupMenu menu = new NonlockingPopupMenu(Strings.message("ui.popup.articles"));

                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_MARK_READ));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_MARK_UNREAD));
                    menu.addSeparator();
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_BROWSE));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_COPY_LINK));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_SAVE_IMAGE));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_POST_TO_BLOG));
                    menu.addSeparator();
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_PROPERTIES));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_TAGS));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_PIN_UNPIN));

                    appendCustomActions(menu, PopupMenuType.IMAGE_ARTICLE);

                    return menu;
                }
            };
        }

        return imageDisplayPopupAdapter;
    }

    public synchronized MouseListener getArticleGroupPopupAdapter()
    {
        if (articleGroupPopupAdapter == null)
        {
            articleGroupPopupAdapter = new PopupAdapter()
            {
                protected JPopupMenu buildPopupMenu(MouseEvent anevent)
                {
                    ArticlesGroup ag = (ArticlesGroup)anevent.getSource();
                    MarkArticlesGroupAction.setGroup(ag);

                    JPopupMenu menu = new NonlockingPopupMenu(Strings.message("ui.popup.article.groups"));

                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLEGROUP_MARK_READ));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLEGROUP_MARK_UNREAD));

                    appendCustomActions(menu, PopupMenuType.ARTICLE_GROUP);

                    return menu;
                }
            };
        }

        return articleGroupPopupAdapter;
    }

    /**
     * Returns popup adapter for article hyper-links.
     *
     * @return popup adapter.
     */
    public synchronized PopupAdapter getArticleHyperLinkPopupAdapter()
    {
        if (articleHyperLinkPopupAdapter == null)
        {
            articleHyperLinkPopupAdapter = new PopupAdapter()
            {
                protected JPopupMenu buildPopupMenu(MouseEvent anevent)
                {
                    JPopupMenu menu = new NonlockingPopupMenu(Strings.message("ui.popup.hyperlinks"));

                    GlobalController controller = GlobalController.SINGLETON;

                    NetworkFeed hoveredFeed = controller.getFeedByHoveredHyperLink();

                    // Set links to the actions as the hovered link will be reset upon
                    // the menu opening as the mouse pointer will move away off the link.
                    URL link = controller.getHoveredHyperLink();
                    HyperLinkOpenAction.setLink(link);
                    HyperLinkCopyAction.setLink(link);
                    HyperLinkSaveAsAction.setLink(link);
                    HyperLinkEmailAction.setLink(link);

                    addBlockImageCommand(menu);

                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_HYPERLINK_OPEN));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_HYPERLINK_COPY));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_HYPERLINK_SAVE_AS));
                    menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_HYPERLINK_SEND));

                    menu.addSeparator();

                    if (hoveredFeed != null)
                    {
                        SelectBlogByLinkAction.setFeed(hoveredFeed);
                        menu.add(ActionManager.get(ActionsTable.CMD_FEED_GOTO_BY_LINK));
                    } else
                    {
                        AddBlogByLinkAction.setLink(link);
                        menu.add(ActionManager.get(ActionsTable.CMD_FEED_SUBSCRIBE_BY_LINK));
                    }

                    appendCustomActions(menu, PopupMenuType.ARTICLE_HYPERLINK);

                    return menu;
                }
            };
        }

        return articleHyperLinkPopupAdapter;
    }

    /**
     * Adds a block image command if that was clicked.
     *
     * @param menu menu.
     */
    private void addBlockImageCommand(JPopupMenu menu)
    {
        URL url = HTMLArticleDisplay.clickImageURL;
        BlockImageAction.setBlockURL(url);
        if (url != null)
        {
            menu.add(ActionManager.get(ActionsTable.CMD_ARTICLE_BLOCK_IMAGE));

            menu.addSeparator();
        }
    }

    /**
     * Appends custom actions to the menu if they are present.
     *
     * @param menu  menu.
     * @param type  type of the hook.
     */
    private void appendCustomActions(JPopupMenu menu, PopupMenuType type)
    {
        java.util.List<Action> actions = new ArrayList<Action>();
        for (IPopupMenuHook hook : popupMenuHooks)
        {
            Collection<Action> pa;

            switch (type)
            {
                case ARTICLE_HYPERLINK:
                    pa = hook.getArticleHyperlinkActions();
                    break;
                case FEEDS_LIST:
                    pa = hook.getFeedsListActions();
                    break;
                case GUIDES_LIST:
                    pa = hook.getGuidesListActions();
                    break;
                case HTML_ARTICLE:
                    pa = hook.getHTMLArticleActions();
                    break;
                case IMAGE_ARTICLE:
                    pa = hook.getImageArticleActions();
                    break;
                case ARTICLE_GROUP:
                    pa = hook.getArticleGroupActions();
                    break;
                default:
                    pa = null;
                    break;
            }

            if (pa != null) actions.addAll(pa);
        }

        if (actions.size() > 0)
        {
            menu.addSeparator();
            for (Action action : actions) menu.add(action);
        }
    }

    /**
     * Focuses on channel list.
     */
    public void setFocusChannelGuide()
    {
        feedsPanel.returnFocusableComponent().requestFocus();
    }

    /**
     * Repaints highlights in articles list.
     */
    public void repaintArticlesListHighlights()
    {
        if (articleListPanel == null) return;

        final IFeedDisplay articleList = articleListPanel.getFeedView();
        if (UifUtilities.isEDT())
        {
            articleList.repaintHighlights();
        } else
        {
            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    articleList.repaintHighlights();
                }
            });
        }
    }

    /**
     * Registers search result to monitor.
     *
     * @param aResult result.
     */
    public void setSearchResult(ISearchResult aResult)
    {
        aResult.addChangesListener(new SearchResultListener());
    }

    /**
     * This class is a replacement of FocustTraversalPolicies. It represents the listener of keyboard events which will
     * be plugged globally. What it does is checking each of key-pressed events for TAB and if it finds it then moves
     * focus to the next "correct" component and consumes original event. <p/>Why things are done in this way? Honestly,
     * I was not able to create correct focus traversal policies for crossing focus cycles. I'll continue my
     * investigations later, but for now this class covers all of the requirements pretty good.
     */
    private class FocusDispatcher implements KeyEventDispatcher
    {

        /**
         * Invoked before any of application handlers check this event.
         *
         * @param e
         *        keyboard event.
         * @return TRUE if event consumed.
         * @see KeyEventDispatcher#dispatchKeyEvent(java.awt.event.KeyEvent)
         */
        public boolean dispatchKeyEvent(final KeyEvent e)
        {
            boolean consumed = false;
            int code = e.getKeyCode();

            if (e.getID() == KeyEvent.KEY_PRESSED)
            {
                if (code == KeyEvent.VK_TAB)
                {
                    if (e.getModifiersEx() == InputEvent.SHIFT_DOWN_MASK)
                    {
                        consumed = focusBackward();
                    } else if (e.getModifiersEx() == 0)
                    {
                        consumed = focusForward();
                    }

                    if (consumed) e.consume();
                } else if (isCopyGuesture(code))
                {
                    DNDListContext.setCopying(true);
                }
            } else if (e.getID() == KeyEvent.KEY_RELEASED && isCopyGuesture(code))
            {
                DNDListContext.setCopying(false);
            }

            return consumed;
        }

        private boolean isCopyGuesture(int aCode)
        {
            return (SystemUtils.IS_OS_MAC && aCode == KeyEvent.VK_ALT) ||
                aCode == KeyEvent.VK_CONTROL;
        }

        /**
         * Pass focus forward.
         *
         * @return TRUE if took care about event.
         */
        private boolean focusForward()
        {
            if (LOG.isLoggable(Level.FINEST)) LOG.finest("Forward");
            final KeyboardFocusManager keyFocusManager =
                KeyboardFocusManager.getCurrentKeyboardFocusManager();

            final Component current = keyFocusManager.getFocusOwner();
            Component next = null;

            // TODO REVIEW !!!
            if (current instanceof IFeedDisplay)
            {
                next = guidesPanel.getFocusableComponent();
            } else if (current == guidesPanel.getFocusableComponent())
            {
                next = feedsPanel.feedsList;
            } else if (current == feedsPanel.feedsList)
            {
                next = articleListPanel.getFeedView().getComponent();
            }

            if (next != null)
            {
                next.requestFocusInWindow();
            }

            return next != null;
        }

        /**
         * Pass focus backward.
         *
         * @return TRUE if took care about event.
         */
        private boolean focusBackward()
        {
            if (LOG.isLoggable(Level.FINEST)) LOG.finest("Backward");

            final KeyboardFocusManager keyFocusManager =
                KeyboardFocusManager.getCurrentKeyboardFocusManager();
            final Component current = keyFocusManager.getFocusOwner();
            Component next = null;

            // TODO REVIEW
            if (current == articleListPanel.getFeedView().getComponent())
            {
                next = feedsPanel.feedsList;
            } else if (current == feedsPanel.feedsList)
            {
                next = guidesPanel.getFocusableComponent();
            } else if (current == guidesPanel.getFocusableComponent())
            {
                next = articleListPanel.getFeedView().getComponent();
            }

            if (next != null)
            {
                next.requestFocusInWindow();
            }

            return next != null;
        }
    }

    /**
     * Tell the Application class what to do on Close of the window.
     */
    protected void configureCloseOperation()
    {
        setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
        addWindowListener(new WindowAdapter()
        {
            @Override
            public void windowClosing(WindowEvent e)
            {
                GlobalController.exitApplication();
            }
        });
    }

    /**
     * Processes window events occurring on this component. Hides the window or disposes of it, as
     * specified by the setting of the <code>defaultCloseOperation</code> property.
     *
     * @param e the window event
     *
     * @see #setDefaultCloseOperation
     * @see java.awt.Window#processWindowEvent
     */
    protected void processWindowEvent(WindowEvent e)
    {
        super.processWindowEvent(e);

        if (SystemUtils.IS_OS_WINDOWS && e.getID() == WindowEvent.WINDOW_DEICONIFIED)
        {
            // The code fragment below fixes bug with incorrect repaint of the deiconified
            // main frame on Win32 platform, meaning, when expanding from iconified to open window.
            repaint();
        }
    }

    /**
     * The UI Framework needs some kind of ID to tell windows apart.
     * Since we only have one window, it doesn't matter what it is.
     *
     * @return ID of the window.
     */
    public String getWindowID()
    {
        return "BlogBridgeMainWindow";
    }

    /**
     * Registers a popup menu hook.
     *
     * @param h hook.
     */
    public void addPopupMenuHook(IPopupMenuHook h)
    {
        if (!popupMenuHooks.contains(h)) popupMenuHooks.add(h);
    }

    /**
     * Removes a popup menu hook.
     *
     * @param h hook.
     */
    public void removePopupMenuHook(IPopupMenuHook h)
    {
        popupMenuHooks.remove(h);
    }

    /**
     * Creates a non-locking menu.
     *
     * @param label label.
     *
     * @return menu.
     */
    public JPopupMenu createNonLockingPopupMenu(String label)
    {
        return new NonlockingPopupMenu(label);
    }

    /**
     * The menu which isn't locking invoker component. Upon hiding it releases the reference
     * allowing it to be normally GC'ed even if this menu instance is cached somewhere.
     */
    private class NonlockingPopupMenu extends JPopupMenu
    {
        /**
         * Constructs a <code>JPopupMenu</code> with the specified title.
         *
         * @param label the string that a UI may use to display as a title for the popup menu.
         */
        public NonlockingPopupMenu(String label)
        {
            super(label);
        }

        /**
         * Displays the popup menu at the position x,y in the coordinate space of the component
         * invoker.
         *
         * @param invoker the component in whose space the popup menu is to appear
         * @param x       the x coordinate in invoker's coordinate space at which the popup menu is to
         *                be displayed
         * @param y       the y coordinate in invoker's coordinate space at which the popup menu is to
         *                be displayed
         */
        public void show(Component invoker, int x, int y)
        {
            super.show(invoker, x, y);

            // Release the invoker
            setInvoker(MainFrame.this);
        }
    }

    /**
     * Monitors the updates in search results and shows/updates popup.
     */
    private class SearchResultListener implements ISearchResultListener
    {
        private int items = 0;
        private static final int MAX_ITEMS = 10;

        /**
         * Invoked when new result item is added to the list.
         *
         * @param result results list object.
         * @param item   item added.
         * @param index  item index.
         */
        public void itemAdded(ISearchResult result, final ResultItem item, int index)
        {
            if (items < MAX_ITEMS)
            {
                items++;
                final boolean showPopup = items == 1;
                SwingUtilities.invokeLater(new Runnable()
                {
                    public void run()
                    {
                        searchPopup.add(item);
                        if (showPopup) showPopup();
                    }
                });
            }
        }

        /**
         * Invoked when the result items are removed from the list.
         *
         * @param result results list object.
         */
        public void itemsRemoved(ISearchResult result)
        {
            items = 0;

            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    hidePopup();
                    searchPopup.removeAll();
                }
            });
        }

        /**
         * Invoked when underlying search is finished.
         *
         * @param result results list object.
         */
        public void finished(ISearchResult result)
        {
        }

        /**
         * Shows popup.
         */
        private void showPopup()
        {
            int mainPaneWidth = mainPane.getSize().width;
            Point loc = new Point(mainPaneWidth - searchPopup.getPreferredSize().width, 0);
            SwingUtilities.convertPointToScreen(loc, mainPane);
            searchPopup.setLocation(loc);
            searchPopup.setVisible(true);
            searchField.requestFocusInWindow();
        }

        /**
         * Hides popup.
         */
        private void hidePopup()
        {
            searchPopup.setVisible(false);
        }
    }
}
TOP

Related Classes of com.salas.bb.views.mainframe.MainFrame

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