// 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);
}
}
}