Package com.salas.bb.views.mainframe

Source Code of com.salas.bb.views.mainframe.FeedsPanel$DraggingListListener

// 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: FeedsPanel.java,v 1.99 2007/09/19 15:55:01 spyromus Exp $
//

package com.salas.bb.views.mainframe;

import com.jgoodies.binding.adapter.BoundedRangeAdapter;
import com.jgoodies.binding.beans.PropertyAdapter;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;
import com.jgoodies.uif.util.SystemUtils;
import com.jgoodies.uifextras.util.UIFactory;
import com.salas.bb.core.*;
import com.salas.bb.domain.*;
import com.salas.bb.domain.prefs.UserPreferences;
import com.salas.bb.utils.StringUtils;
import com.salas.bb.utils.dnd.DNDList;
import com.salas.bb.utils.dnd.DNDListContext;
import com.salas.bb.utils.dnd.IDNDObject;
import com.salas.bb.utils.i18n.Strings;
import com.salas.bb.utils.uif.*;
import com.salas.bb.views.settings.RenderingManager;
import com.salas.bb.views.settings.RenderingSettingsNames;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.net.URL;
import java.text.MessageFormat;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Display information about all the Channels that are part of the selected ChannelGuide.
*/
public class FeedsPanel extends CoolInternalFrame
{
    private static final Logger LOG = Logger.getLogger(FeedsPanel.class.getName());

    protected static final int INITIAL_VISIBLE_ROWS     = 15;
    protected static final int FIXED_CHAN_CELL_WIDTH    = 185;
    protected static final int FIXED_CHAN_CELL_HEIGHT   = 35;

    /** Pause in ms between progress icon frames. */
    private static final int PROGRESS_ICON_FRAME_PAUSE = 750;

    protected DNDList               feedsList;
    protected JScrollPane           scrollPane;
    private FeedsListCellRenderer   cellRenderer;
    private UnreadActivityController    activityController;
    private JLabel lbNoGuideSelected;

    private Action onDoubleClickAction;

    /**
     * Constructor.
     */
    public FeedsPanel()
    {
        super(Strings.message("panel.feeds"));

        lbNoGuideSelected = new JLabel(Strings.message("panel.feeds.no.guide.selected"));
        lbNoGuideSelected.setHorizontalAlignment(SwingUtilities.CENTER);

        // Create and register toolbar
        setHeaderControl(createSubtoolbar());

        GlobalModel globalModel = GlobalModel.SINGLETON;

        GuideModel model = globalModel.getGuideModel();
        feedsList = new DNDList(model);
        model.setListComponent(feedsList);

        // Set the background
        setBackground(feedsList.getBackground());

        // Register own controller listener
        final ControllerListener l = new ControllerListener();
        GlobalController.SINGLETON.addControllerListener(l);

        setPreferredSize(new Dimension(FIXED_CHAN_CELL_WIDTH, FIXED_CHAN_CELL_HEIGHT));

        UserPreferences prefs = globalModel.getUserPreferences();
        long delay = prefs.getFeedSelectionDelay();
        FeedSelectionListener selListener = new FeedSelectionListener(delay);
        prefs.addPropertyChangeListener(UserPreferences.PROP_FEED_SELECTION_DELAY, selListener);
        feedsList.addListSelectionListener(selListener);
        feedsList.addMouseListener(selListener);

        // Subscribe to theme and layout changes notifications
        RenderingManager.addPropertyChangeListener(new RenderSettingsChangeListener());

        new LoadingIconRepainter(feedsList, PROGRESS_ICON_FRAME_PAUSE).start();
        cellRenderer = new FeedsListCellRenderer();
        onListColorsUpdate();

        feedsList.setCellRenderer(cellRenderer);
        feedsList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
        feedsList.setVisibleRowCount(INITIAL_VISIBLE_ROWS);
//        feedsList.setBorder(new EmptyBorder(2, 2, 2, 2));
        Dimension cellSize = cellRenderer.getFixedCellSize();
        feedsList.setFixedCellWidth(cellSize.width);
        feedsList.setFixedCellHeight(cellSize.height);

        scrollPane = UIFactory.createStrippedScrollPane(feedsList);
        scrollPane.setMinimumSize(new Dimension(cellSize.width + 55, cellSize.height));

        scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
        scrollPane.setViewportView(feedsList);
        add(scrollPane);

        // For the Channel List itself
        final MainFrame mainFrame = GlobalController.SINGLETON.getMainFrame();
        feedsList.addMouseListener(mainFrame.getFeedsListPopupAdapter());

        // For the header of the ChannelListScrollarea
        addMouseListener(mainFrame.getFeedsListPopupAdapter());

        final FeedsListListener listener = new FeedsListListener(this);

        feedsList.addMouseListener(listener);
        feedsList.addMouseMotionListener(listener);

        // Enable drag'n'drop
        feedsList.addPropertyChangeListener(DNDList.PROP_DRAGGING, new DraggingListListener());
        feedsList.setDropTarget(new URLDropTarget(new URLDropListener()));

        activityController = new UnreadActivityController(this);

        FeedDisplayModeManager.getInstance().addListener(new IDisplayModeManagerListener()
        {
            public void onClassColorChanged(int feedClass, Color oldColor, Color newColor)
            {
                feedsList.repaint();
            }
        });

        // Select no guide initially
        l.guideSelected(null);
    }

    /**
     * Sets on-double-click action.
     *
     * @param action action.
     */
    public void setOnDoubleClickAction(Action action)
    {
        this.onDoubleClickAction = action;
    }

    /**
     * Creates sub-toolbar component.
     *
     * @return component.
     */
    private JComponent createSubtoolbar()
    {
        UserPreferences uPrefs = GlobalModel.SINGLETON.getUserPreferences();
        String propName = UserPreferences.PROP_GOOD_CHANNEL_STARZ;
        PropertyAdapter propertyAdapter = new PropertyAdapter(uPrefs, propName, true);
        BoundedRangeAdapter model = new BoundedRangeAdapter(propertyAdapter, 0, 1, 5);

        StarsSelectionComponent starsSelector = new StarsSelectionComponent(model);
        starsSelector.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));

        // Install Starz Selector and its proactive tip
        MouseListener tipAdapter = new TipOfTheDay.TipMouseAdapter(TipOfTheDay.TIP_STARZ_FILTER, true);
        starsSelector.addMouseListener(tipAdapter);

        JPanel pnl = new JPanel(new BorderLayout());
        pnl.add(starsSelector, BorderLayout.CENTER);

        return pnl;
    }

    /**
     * Shows the list of feeds or the "No Guide Selected" message depending on
     * current guide selection.
     *
     * @param selectedGuide current guide selection.
     */
    private void updateMainListArea(IGuide selectedGuide)
    {
        if (selectedGuide == null)
        {
            remove(scrollPane);
            add(lbNoGuideSelected);
        } else
        {
            remove(lbNoGuideSelected);
            add(scrollPane);
        }

        revalidate();
        repaint();
    }

    /**
     * Selects item in list.
     *
     * @param feed feed to select.
     */
    public void selectListItem(final IFeed feed)
    {
        if (SwingUtilities.isEventDispatchThread())
        {
            selectListItem0(feed);
        } else
        {
            SwingUtilities.invokeLater(new SelectFeed(feed));
        }
    }

    /**
     * Selects item in list.
     *
     * @param feed to select.
     */
    private void selectListItem0(IFeed feed)
    {
        GuideModel model = (GuideModel)feedsList.getModel();
        synchronized (model)
        {
            int index = model.indexOf(feed);
            if (index > -1)
            {
                // Select item only if it isn't already selected (multi-selections support)
                if (!feedsList.isSelectedIndex(index))
                {
                    feedsList.setSelectedIndex(index);
                    feedsList.ensureIndexIsVisible(index);
                }
            } else
            {
                feedsList.clearSelection();
            }
        }
    }

    /**
     * Returns feeds list component.
     *
     * @return feeds list component.
     */
    public JList getFeedsList()
    {
        return feedsList;
    }

    /**
     * Returns the monitor which manages the appearance of the freshness button.
     *
     * @return freshness button monitor
     */
    public UnreadActivityController getUnreadActivityController()
    {
        return activityController;
    }

    /**
     * Returns focusable component of the list.
     *
     * @return component.
     */
    public Component returnFocusableComponent()
    {
        return feedsList;
    }

    /**
     * Updates the list color according to the theme and starts list repainting.
     */
    private void onListColorsUpdate()
    {
        Color background = RenderingManager.getFeedsListBackground(false);
        feedsList.setBackground(background);
        feedsList.repaint();
    }

    /**
     * Update the layout of the feeds list cells.
     */
    private void updateFeedsListLayout()
    {
        cellRenderer.initLayout();
        Dimension size = cellRenderer.getFixedCellSize();
        feedsList.setFixedCellWidth(size.width);
        feedsList.setFixedCellHeight(size.height);
        feedsList.repaint();
        activityController.resetAttachment();
    }

    /**
     * Listens for URL drops and invokes feeds subscriptions.
     */
    private class URLDropListener implements IURLDropTargetListener
    {
        /**
         * Called when valid URL is dropped to the target.
         *
         * @param url       URL dropped.
         * @param location  mouse pointer location.
         */
        public void urlDropped(URL url, Point location)
        {
            if (GlobalController.SINGLETON.checkForNewSubscription()) return;

            final GlobalController controller = GlobalController.SINGLETON;
            final IFeed feed = controller.createDirectFeed(url.toString(), false);

            if (feed != null)
            {
                // We do this to be sure that all events connected to addition of
                // the feed to the guide are successfully processed.
                SwingUtilities.invokeLater(new Runnable()
                {
                    public void run()
                    {
                        controller.selectFeed(feed);
                    }
                });
            }
        }
    }

    /**
     * Listens for dragging mode changes of the list and updates global context.
     */
    private class DraggingListListener implements PropertyChangeListener
    {
        /**
         * This method gets called when a bound property is changed.
         *
         * @param evt A PropertyChangeEvent object describing the event source and the property that
         *            has changed.
         */
        public void propertyChange(final PropertyChangeEvent evt)
        {
            final IGuide sourceGuide = GlobalModel.SINGLETON.getSelectedGuide();

            boolean isDraggingFinished = !(Boolean)evt.getNewValue();
            if (isDraggingFinished && sourceGuide instanceof StandardGuide)
            {
                final DNDList source = DNDListContext.getSource();
                IDNDObject object = DNDListContext.getObject();
                int insertPosition = source.getInsertPosition();
                final Object[] feedsI = object.getItems();
                StandardGuide guide = (StandardGuide)sourceGuide;

                if (feedsList.isDraggingInternal())
                {
                    final GuideModel model = (GuideModel)feedsList.getModel();

                    // Dragging operation finished within the same list
                    final IFeed currentSelection = GlobalModel.SINGLETON.getSelectedFeed();

                    int index = insertPosition;
                    for (int i = 0; i < feedsI.length; i++)
                    {
                        IFeed feed = (IFeed)feedsI[feedsI.length - i - 1];

                        int currentIndex = model.indexOf(feed);
                        if (currentIndex < index) index--;

                        GlobalController.SINGLETON.moveFeed(feed, guide, guide, index);
                    }

                    // We call it in new EDT task as the model will be updated
                    // in the next event only, so we have to schedule ourselves
                    // after that update to get correct indexes.
                    SwingUtilities.invokeLater(new Runnable()
                    {
                        public void run()
                        {
                            int curSelNewIndex = model.indexOf(currentSelection);

                            boolean curSelIsOnTheList = false;
                            int[] newIndices = new int[feedsI.length];
                            for (int i = 0; i < feedsI.length; i++)
                            {
                                newIndices[i] = model.indexOf((IFeed)feedsI[i]);
                                curSelIsOnTheList |= newIndices[i] == curSelNewIndex;
                            }

                            ListSelectionModel selModel = source.getSelectionModel();
                            if (!curSelIsOnTheList)
                            {
                                selModel.setSelectionInterval(curSelNewIndex, curSelNewIndex);
                            } else
                            {
                                source.setSelectedIndices(newIndices);
                            }

                            selModel.setLeadSelectionIndex(curSelNewIndex);

                            // Return focus to the guide
                            GlobalController.SINGLETON.fireGuideSelected(sourceGuide);
                        }
                    });
                } else
                {
                    Object destination = DNDListContext.getDestination();
                    boolean isCopying = DNDListContext.isFinishedCopying();

                    if (destination instanceof StandardGuide &&
                        destination != sourceGuide)
                    {
                        StandardGuide destGuide = (StandardGuide)destination;

                        // Feeds should be moved to the new guide.
                        for (Object f : feedsI)
                        {
                            IFeed feed = (IFeed)f;
                            if (isCopying)
                            {
                                destGuide.add(feed);
                            } else
                            {
                                GlobalController.SINGLETON.moveFeed(feed,
                                    guide, destGuide, destGuide.getFeedsCount());
                            }
                        }

                        // EDT !!!
                        GlobalController.SINGLETON.fireGuideSelected(guide);
                        if (guide.getFeedsCount() > 0)
                        {
                            GlobalController.SINGLETON.selectFeed(guide.getFeedAt(0));
                        }
                    }
                }
            }
        }
    }

    /**
     * The UnreadActivityController manages the unread button and activity meter. There is a single
     * "live" instance of each, which is moved into place on whatever row the user has moused on.
     * We take care to remove or reset the buttons if the table contents changes, so that they're
     * not left in an obsolete position in the list.
     */
    static class UnreadActivityController extends ComponentAdapter
        implements ListDataListener, ActionListener
    {
        private static final String TOOLTIP_MSG_SINGLE = Strings.message("panel.feeds.unread.one");
        private static final String TOOLTIP_MSG_MANY = Strings.message("panel.feeds.unread.many");

        private JList feedsList;
        private ArticleActivityMeter activityMeter;
        private UnreadButton unreadButton;
        private int attachedRow;
        private IFeed attachedFeed;

        /**
         * Constructs as on the given FeedsPanel.
         * @param thePanel Panel we're attached to
         */
        UnreadActivityController(FeedsPanel thePanel)
        {
            feedsList = thePanel.getFeedsList();
            activityMeter = new ArticleActivityMeter();
            unreadButton = new UnreadButton();
            unreadButton.initToolTipMessage(TOOLTIP_MSG_SINGLE, TOOLTIP_MSG_MANY);
            attachedRow = -1;

            attachListeners();
        }

        /**
         * Adds listeners so that we are notified of changes to the list, and can track the button
         * press on the unread button.
         */
        void attachListeners()
        {
            GlobalModel.SINGLETON.getGuideModel().addListDataListener(this);
            unreadButton.addActionListener(this);
            feedsList.addComponentListener(this);
        }

        /**
         * Compute the unread statistics for the given feed, i.e. the number of read/unread
         * articles over the last X days.
         * @param feed The feed to calculate.
         * @return the UnreadStats for that feed.
         */
        static UnreadStats calcUnreadStats(IFeed feed)
        {
            UnreadStats stats = new UnreadStats();

            IArticle[] articles = feed.getArticles();
            for (IArticle art : articles)
            {
                stats.increment(art.getPublicationDate(), art.isRead());
            }
            return stats;
        }

        /**
         * Move the buttons into place on the given row of the list. Does nothing if we're already
         * attached at that position.
         * @param row index of row to attach to
         * @param forceUpdate <code>TRUE</code> to force button update even if already in place
         */
        void attachButtons(int row, boolean forceUpdate)
        {
            boolean showUnread = RenderingManager.isShowUnreadInFeeds();
            boolean showActivity = RenderingManager.isShowActivityChart();
            boolean showStarz = RenderingManager.isShowStarz();
            boolean showOneRow = !(showActivity || showStarz);

            IFeed feed = (IFeed) feedsList.getModel().getElementAt(row);

            boolean sameButton = (row == attachedRow && feed == attachedFeed);

            if (sameButton && !forceUpdate) return;

            attachedFeed = feed;
            attachedRow = row;
            UnreadStats stats = calcUnreadStats(attachedFeed);
            activityMeter.init(stats);

            Rectangle cellBounds  = feedsList.getCellBounds(row, row);
            Rectangle r = new Rectangle(cellBounds);

            r.x = r.width - 2;
            // Cell layout is slightly different on Mac -- see FeedListCellRender
            r.y += SystemUtils.IS_OS_MAC ? 3 : 1;

            if (showActivity)
            {
                r.x -= activityMeter.getSize().width;

                r.setSize(activityMeter.getSize());
                activityMeter.setBounds(r);
                feedsList.add(activityMeter);
            } else
            {
                feedsList.remove(activityMeter);

                if (showOneRow) r.x += 1;
            }

            if (showUnread)
            {
                // unread button is immediately to the left of the activity
                // meter, centered vertically in the row
                Dimension unreadButtonSize = unreadButton.getSize();
                r.x -= unreadButtonSize.width;
                r.setSize(unreadButtonSize);
                r.y = cellBounds.y + (SystemUtils.IS_OS_MAC ? 3 : 1);
                if (showOneRow) r.y += 1;

                int unreadCount = stats.getTotalCount().getUnread();
                Rectangle oldBounds = unreadButton.getBounds();

                // If it's the same button at the same position, then update it;
                // this preserves the mouse state of the button.  Otherwise,
                // reset it.
                if (sameButton && oldBounds.equals(r))
                {
                    unreadButton.update(unreadCount);
                } else
                {
                    unreadButton.setBounds(r);
                    unreadButton.init(stats.getTotalCount().getUnread());
                }
                feedsList.add(unreadButton);

                // Register the object this button is attached to (for event)
                unreadButton.setAttachedToObject(attachedFeed);
            } else
            {
                feedsList.remove(unreadButton);
            }
        }

        /**
         * Detach the buttons from the component hierarchy, effectively hiding them.
         */
        void detachButtons()
        {
            if (activityMeter.getParent() != null)
            {
                // For some reason an explicit repaint is required to
                // paint over the old button. (Problem is only noticable when
                // button had been displayed in a raised state when detached.)
                Rectangle r = activityMeter.getBounds();
                feedsList.remove(activityMeter);
                feedsList.repaint(r);
            }

            if (unreadButton.getParent() != null)
            {
                Rectangle r = unreadButton.getBounds();
                feedsList.remove(unreadButton);
                feedsList.repaint(r);
            }
            attachedFeed = null;
            attachedRow = -1;
        }

        /**
         * Validate that the buttons are attached in the proper position and show up-to-date
         * article read/unread info.
         */
        void resetAttachment()
        {
            if (attachedRow >= 0)
            {
                int row = attachedRow;
                IFeed feed = attachedFeed;

                // reattach to update them if row is still valid for feed
                ListModel model = feedsList.getModel();
                if (model.getSize() > row && model.getElementAt(row) == feed)
                    attachButtons(row, true);
                else
                    detachButtons();
            }
        }

        /**
         * Notifies us that contents of list have changed. Update button -- unread counts could
         * have changed.
         * @see javax.swing.event.ListDataListener#contentsChanged(javax.swing.event.ListDataEvent)
         */
        public void contentsChanged(ListDataEvent e)
        {
            resetAttachment();
        }

        /**
         * Elements have been added to the list - reset button attachment.
         * @see javax.swing.event.ListDataListener#intervalAdded(javax.swing.event.ListDataEvent)
         */
        public void intervalAdded(ListDataEvent e)
        {
            resetAttachment();
        }

        /**
         * Elements have been removed from the list - reset button attachment.
         * @see javax.swing.event.ListDataListener#intervalRemoved(javax.swing.event.ListDataEvent)
         */
        public void intervalRemoved(ListDataEvent e)
        {
            resetAttachment();
        }

        /**
         * The list has been resized - - reset button attachment.
         * @see java.awt.event.ComponentListener#componentResized(java.awt.event.ComponentEvent)
         */
        public void componentResized(ComponentEvent e)
        {
            resetAttachment();
        }

        /**
         * Handle a press of the unread button. (non-Javadoc)
         * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
         */
        public void actionPerformed(ActionEvent e)
        {
            GlobalModel model = GlobalModel.SINGLETON;

            IFeed feed = (IFeed)e.getSource();
            GlobalController.readFeeds(true, model.getSelectedGuide(), feed);
        }
    }

    /**
     * Simple listener for FeedsList to make sure that a right click menu also has the effect of selecting the indicated
     * item.
     */
    class FeedsListListener extends MouseAdapter implements MouseMotionListener
    {
        private static final int ICON_STARS_WIDTH = 64;

        private FeedsPanel feedsPanel;
        private JList feedsList;
        private GuideModel model;
        private MouseListener starzSettingTipAdapter;

        private int insetsTop;

        FeedsListListener(final FeedsPanel thePanel)
        {
            feedsPanel = thePanel;
            feedsList = thePanel.getFeedsList();

            model = (GuideModel)feedsList.getModel();
            insetsTop = feedsList.getInsets().top;

            starzSettingTipAdapter =
                new TipOfTheDay.TipMouseAdapter(TipOfTheDay.TIP_STARZ_SETTINGS, true);
        }

        /**
         * If this is a right click then select the corresponding row in the list.
         *
         * @param e event object.
         */
        public void mousePressed(final MouseEvent e)
        {
            Point point = e.getPoint();
            int row = feedsList.locationToIndex(point);
            if (row != -1 && feedsList.getCellBounds(row, row).contains(point))
            {
                if (SwingUtilities.isRightMouseButton(e))
                {
                    if (!feedsList.isSelectedIndex(row)) feedsList.setSelectedIndex(row);
                } else if (SwingUtilities.isLeftMouseButton(e))
                {
                    if (FeedsListCellRenderer.getHoveredStar() != -1)
                    {
                        starzSettingTipAdapter.mousePressed(e);

                        IFeed feed = (IFeed)model.getElementAt(row);
                        int rating = (e.getModifiers() & InputEvent.SHIFT_MASK) != 0 ? -1
                            : FeedsListCellRenderer.getHoveredStar();

                        if (feed.getRating() != rating)
                        {
                            feed.setRating(rating);

                            // If channel is no longer selectable then reset selection
                            if (!model.isPresent(feed))
                            {
                                GlobalController.SINGLETON.selectFeed(null);
                            }
                        }
                    }
                }
            }
        }

        private Point convertToCellCoords(Point point)
        {
            final Rectangle rect = feedsList.getCellBounds(0, 0);
            int y = (point.y - insetsTop) % rect.height;
            int x = point.x - feedsList.getInsets().left;

            return new Point(x, y);
        }

        private int locationToStar(Point point)
        {
            return (int)((point.x - 3) / (ICON_STARS_WIDTH / 5.0));
        }

        /**
         * Invoked when the mouse enters a component.
         */
        public void mouseEntered(final MouseEvent e)
        {
            checkHover(e.getPoint());
        }

        /**
         * Invoked when a mouse button is pressed on a component and then dragged.
         * <code>MOUSE_DRAGGED</code> events will continue to be delivered to the component where
         * the drag originated until the mouse button is released (regardless of whether the mouse
         * position is within the bounds of the component). <p/>Due to platform-dependent Drag&Drop
         * implementations, <code>MOUSE_DRAGGED</code> events may not be delivered during a native
         * Drag&Drop operation.
         */
        public void mouseDragged(final MouseEvent e)
        {
            checkHover(e.getPoint());
        }

        /**
         * Invoked when the mouse cursor has been moved onto a component but no buttons have been
         * pushed.
         */
        public void mouseMoved(final MouseEvent e)
        {
            checkHover(e.getPoint());
        }

        /**
         * Check if we require to mark some hover and to unmark some.
         *
         * @param point mouse pointer position.
         */
        private void checkHover(final Point point)
        {
            int cursor = Cursor.DEFAULT_CURSOR;
            int row = feedsList.locationToIndex(point);

            int star = -1;
            IFeed hoveredFeed = null;

            // Check if pointer over the rating icon
            if (row > -1 && feedsList.getCellBounds(row, row).contains(point))
            {
                // hover new rating icon
                hoveredFeed = (IFeed)feedsList.getModel().getElementAt(row);

                final Point convertedPoint = convertToCellCoords(point);
                if (cellRenderer.isStarzHovered(convertedPoint) &&
                    ((hoveredFeed instanceof DataFeed && ((DataFeed)hoveredFeed).isInitialized()) ||
                    (hoveredFeed instanceof SearchFeed)))
                {
                    boolean selectedCell = feedsList.getSelectedIndex() == row;
                    if (selectedCell)
                    {
                        star = locationToStar(convertedPoint);
                        cursor = Cursor.HAND_CURSOR;
                    }
                }

                feedsPanel.getUnreadActivityController().attachButtons(row, false);
            }

            FeedsListCellRenderer.setHoveredStar(star);
            FeedsListCellRenderer.setHoveredFeed(hoveredFeed);

            if (feedsList.getCursor().getType() != cursor)
            {
                feedsList.setCursor(Cursor.getPredefinedCursor(cursor));
            }
        }


        /**
         * Invoked when mouse clicks over the list.
         *
         * @param e event.
         */
        public void mouseClicked(MouseEvent e)
        {
            if (onDoubleClickAction != null && e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e))
            {
                onDoubleClickAction.actionPerformed(new ActionEvent(feedsList, 0, null));
            }
        }
    }

    /**
     * Listens for <code>GlobalController</code> events in order to get when channel selected. This
     * information is necessary to set correct articles list title.
     */
    private final class ControllerListener extends ControllerAdapter
    {
        /**
         * Invoked after application changes the channel.
         *
         * @param guide guide to which we are switching.
         */
        public void guideSelected(final IGuide guide)
        {
            final String text = (guide == null ? Strings.message("panel.feeds.no.guide.selected") : guide.getTitle());

            setSubtitle(MessageFormat.format(Strings.message("panel.in"), text));
            updateMainListArea(guide);
        }
    }

    /**
     * Calls model to fire updates of the cells once in specified interval. Cells are registered
     * during the waiting time and cleared once fired.
     */
    private static class LoadingIconRepainter extends Thread
    {
        private JList       list;
        private long        intervals;

        /**
         * Creates thread for repainting of loading icons.
         *
         * @param aList         list to monitor.
         * @param aIntervals    intervals of updates.
         */
        public LoadingIconRepainter(JList aList, long aIntervals)
        {
            super(LoadingIconRepainter.class.getName());
            setDaemon(true);

            list = aList;
            intervals = aIntervals;
        }

        /**
         * Repaint all feeds which require repainting.
         */
        private synchronized void repaintFeedsRows()
        {
            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    ListModel model = list.getModel();
                    int firstVisibleIndex = list.getFirstVisibleIndex();
                    int lastVisibleIndex = list.getLastVisibleIndex();

                    if (firstVisibleIndex >= 0 && lastVisibleIndex >= 0)
                    {
                        for (int i = firstVisibleIndex; i <= lastVisibleIndex; i++)
                        {
                            IFeed feed = (IFeed)model.getElementAt(i);
                            if (FeedsListCellRenderer.needsProgressIcon(feed))
                            {
                                list.repaint(list.getCellBounds(i, i));

                                // The commented method of cell repainting is too dirty and expensive because
                                // it involves calls within the model and many blocks happen
//                                ((GuideModel)model).fireContentsChanged(model, i, i);
                            }
                        }
                    }
                }
            });
        }

        /**
         * Main thread cycle.
         */
        public void run()
        {
            while (true)
            {
                try
                {
                    repaintFeedsRows();
                    try
                    {
                        Thread.sleep(intervals);
                    } catch (InterruptedException e)
                    {
                        LOG.log(Level.WARNING, Strings.error("interrupted"), e);
                    }
                } catch (Exception e)
                {
                    LOG.log(Level.SEVERE, Strings.error("unhandled.exception"), e);
                }
            }
        }
    }

    /**
     * Custom Renderer for entries in the channelList.
     */
    private static class FeedsListCellRenderer extends JPanel implements ListCellRenderer
    {
        private static final Logger LOG = Logger.getLogger(FeedsListCellRenderer.class.getName());

        private static final Color COLOR_DRAG_SOURCE = Color.LIGHT_GRAY;
        private static final Border BORDER_NO_FOCUS = BorderFactory.createEmptyBorder(1, 1, 1, 1);

        private JLabel lbStars;     // Icon for overall 'stars' ranking
        private JLabel lbLoading;   // Icon for loading process indication
        private JLabel lbTitle;     // Text for Title
        private JLabel lbIcon;      // Icon
        private ArticleActivityMeter activityMeter;
        private UnreadButton        unreadButton;

        private static int hoveredStar = -1;

        private static IFeed hoveredFeed;

        /**
         * Creates new cell renderer.
         */
        public FeedsListCellRenderer()
        {
            super();

            lbTitle = new JLabel();
            activityMeter = new ArticleActivityMeter();
            unreadButton = new UnreadButton();
            lbStars = new JLabel();
            lbLoading = new JLabel();
            lbIcon = new JLabel();
            lbIcon.setHorizontalAlignment(SwingConstants.CENTER);
            lbIcon.setPreferredSize(new Dimension(18, 16));

            setOpaque(true);
            setBorder(BORDER_NO_FOCUS);

            initLayout();

            if (LOG.isLoggable(Level.FINE)) LOG.fine("Completed construction");
        }

        /**
         * Initialize the layout of the list based on current
         * rendering options.
         */
        public void initLayout()
        {
            boolean showStarz = RenderingManager.isShowStarz();
            boolean showUnread = RenderingManager.isShowUnreadInFeeds();
            boolean showActivity = RenderingManager.isShowActivityChart();
            boolean showOneRow = !(showStarz || showActivity);
            boolean mac = SystemUtils.IS_OS_MAC;

            CellConstraints cc = new CellConstraints();
            String spacing = mac ? "2px" : "0";
            if (showOneRow)
            {
                // - title - spin - antenna - unread
                String cols = "1px, left:64px:grow, 1px, 12px, 2px, p, 1px, center:21px, 1px";
                String rows = spacing + ", max(16px;pref), " + spacing;
                setLayout(new FormLayout(cols, rows));

                add(lbTitle, cc.xy(2, 2));
                add(lbLoading, cc.xy(4, 2));
                add(lbIcon, cc.xy(6, 2));
                manageComponent(unreadButton, showUnread, cc.xy(8, 2));
                remove(lbStars);
                remove(activityMeter);
            } else
            {
                // - starz/title - spin - unread/untenna - activity
                String cols = "1px, left:64px:grow, 1px, 12px, 2px, center:21px, 1px, pref, 1px";
                String rows = spacing + ", max(12px;p), 1px, max(16px;pref), " + spacing;
                setLayout(new FormLayout(cols, rows));

                add(lbTitle, cc.xyw(2, 4, 3));
                add(lbLoading, cc.xy(4, 2));
                add(lbIcon, cc.xy(6, 4));
                manageComponent(unreadButton, showUnread, cc.xy(6, 2));
                manageComponent(lbStars, showStarz, cc.xy(2, 2));
                manageComponent(activityMeter, showActivity, cc.xywh(8, 2, 1, 3, "right, top"));
            }

            Font textFont = mac
                ? new Font("Lucida Grande", Font.BOLD, 10)
                : lbTitle.getFont().deriveFont(Font.BOLD);
            lbTitle.setFont(textFont);
        }

        /**
         * Adds or removes a component depending on its show-state.
         *
         * @param component component.
         * @param show      state.
         * @param constr    constraints.
         */
        private void manageComponent(JComponent component, boolean show, Object constr)
        {
            if (show)
            {
                add(component, constr);
            } else
            {
                remove(component);
            }
        }

        /*
        * Returns the fixed cell size.
        * If the cell renderer gets more complicated, we may have to create a
        * more elaborate cell mock-up in order to measure the size.
        */
        public Dimension getFixedCellSize()
        {
            lbTitle.setText("<dummy>"); // so title row has some height
            validate();
            return getPreferredSize();
        }

        /**
         * Sets the hovered feed.
         *
         * @param aHoveredFeed hovered feed.
         */
        public static void setHoveredFeed(IFeed aHoveredFeed)
        {
            hoveredFeed = aHoveredFeed;
        }

        /**
         * Sets the hovered star.
         *
         * @param aStar star.
         */
        public static void setHoveredStar(int aStar)
        {
            hoveredStar = aStar;
        }

        /**
         * Returns the hovered star.
         *
         * @return star.
         */
        public static int getHoveredStar()
        {
            return hoveredStar;
        }

        /**
         * Return a component that has been configured to display the specified value. That
         * component's <code>paint</code> method is then called to "render" the cell. If it is
         * necessary to compute the dimensions of a list because the list cells do not have a fixed
         * size, this method is called to generate a component on which
         * <code>getPreferredSize</code> can be invoked.
         *
         * @param list         The JList we're painting.
         * @param value        The value returned by list.getModel().getElementAt(index).
         * @param index        The cells index.
         * @param isSelected   True if the specified cell was selected.
         * @param cellHasFocus True if the specified cell has the focus.
         *
         * @return A component whose paint() method will render the specified value.
         *
         * @see javax.swing.JList
         * @see javax.swing.ListSelectionModel
         * @see javax.swing.ListModel
         */
        public Component getListCellRendererComponent(final JList list, final Object value,
                                                      final int index, final boolean isSelected, final boolean cellHasFocus)
        {
            // Setup the values
            IFeed currentFeed = (IFeed)value;

            if (currentFeed == null) return null;

            // Based on selection, choose foreground and background colors.
            Color backround = (index != -1 && isBeingDragged(value))
                ? COLOR_DRAG_SOURCE
                : isSelected
                    ? RenderingManager.getFeedsListSelectedBackground()
                    : RenderingManager.getFeedsListBackground(index % 2 == 0);

            Color foreground = FeedDisplayModeManager.getInstance().getColor(currentFeed, isSelected);
            if (foreground == null || isSelected)
            {
                foreground = RenderingManager.getFeedsListForeground(isSelected);
            }

            setForeground(foreground);
            setBackground(backround);
            lbTitle.setForeground(foreground);
            lbTitle.setBackground(backround);
            lbStars.setForeground(Color.RED);
            lbStars.setBackground(Color.RED);

            // Indicate focus with a border
            setBorder((cellHasFocus)
                ? UIManager.getBorder("List.focusCellHighlightBorder")
                : BORDER_NO_FOCUS);

            FeedFormatter formatter = new FeedFormatter(currentFeed);

            String title = currentFeed.getTitle();

            // Find appropriate icon
            String type = null;
            if (currentFeed.isDynamic())
            {
                type = "feed.from.reading.list.icon";
            } else if (currentFeed instanceof QueryFeed)
            {
                type = "feed.query.icon";
            } else if (currentFeed instanceof SearchFeed)
            {
                type = "feed.search.icon";
            }
            Icon icon = type == null ? null : IconSource.getIcon(type);

            // Don't let the title string be empty, or FormLayout will consider
            // it 0 height and collapse ALL the title rows, in the case that this is the
            // first title in the list.
            if (title.length() == 0) title = Strings.message("panel.feeds.no.title");
            lbTitle.setText(title);
            lbIcon.setIcon(icon);

            UnreadStats stats = UnreadActivityController.calcUnreadStats(currentFeed);

            activityMeter.init(stats);
            unreadButton.init(stats.getTotalCount().getUnread());

            Icon iconStars = null;
            Icon iconLoading = null;

            if ((currentFeed instanceof DataFeed && ((DataFeed)currentFeed).isInitialized()) ||
                 currentFeed instanceof SearchFeed)
            {
                iconStars = formatter.getStarsIcon();
            }

            if (needsProgressIcon(currentFeed))
            {
                long time = System.currentTimeMillis();
                int frames = FeedFormatter.getLoadingIconFrames();
                int frame = (int)((time / PROGRESS_ICON_FRAME_PAUSE) % frames);
                iconLoading = FeedFormatter.getLoadingIcon(frame);
            }

            lbStars.setIcon(iconStars);
            lbLoading.setIcon(iconLoading);

            // Finally make the title bold only some articles have not yet been read.
            Font fnt = this.lbTitle.getFont();
            int style = currentFeed.isRead() ? Font.PLAIN : Font.BOLD;
            this.lbTitle.setFont(fnt.deriveFont(style));

            return this;
        }

        /**
         * Returns <code>TRUE</code> if value in the list of items being dragged at the moment.
         *
         * @param aValue value to check.
         *
         * @return <code>TRUE</code> if value in the list of items being dragged at the moment.
         */
        private boolean isBeingDragged(Object aValue)
        {
            boolean found = false;

            if (DNDListContext.isDragging())
            {
                Object[] items = DNDListContext.getObject().getItems();
                for (int i = 0; !found && i < items.length; i++)
                {
                    found = aValue == items[i];
                }
            }

            return found;
        }

        /**
         * Returns <code>TRUE</code> if feed needs progress indicator icon to be displayed.
         *
         * @param feed  feed to check.
         *
         * @return <code>TRUE</code> if feed needs progress indicator icon to be displayed.
         */
        static boolean needsProgressIcon(IFeed feed)
        {
            if (!GlobalController.getConnectionState().isOnline()) return false;

            boolean repaint = feed.isProcessing();

            if (!repaint && feed instanceof DirectFeed)
            {
                FeedMetaDataHolder holder = ((DirectFeed)feed).getMetaDataHolder();
                repaint = holder == null || !holder.isComplete();
            }

            return repaint;
        }

        /**
         * Returns the string to be used as the tooltip for <i>event </i>. By default this returns
         * any string set using <code>setToolTipText</code>. If a component provides more extensive
         * API to support differing tooltips at different locations, this method should be
         * overridden.
         *
         * @param event event object.
         * @return the tooltip message string
         */
        public String getToolTipText(final MouseEvent event)
        {
            if (hoveredFeed == null) return null;

            String text = null;

            Rectangle bounds = getBounds();
            setSize(-bounds.x, -bounds.y);

            Component comp = getComponentAt(event.getPoint());
            if (comp == lbStars)
            {
                GlobalModel model = GlobalModel.SINGLETON;

                int rating = hoveredFeed.getRating();
                int score = model.getScoreCalculator().calcBlogStarzScore(hoveredFeed);

                String name = covertToResources(FeedFormatter.getStarzFileName(score, true));
                String userRatingName = null;

                boolean userRatingSet = rating != -1;

                if (userRatingSet)
                {
                    userRatingName = FeedFormatter.getStarzFileName(rating, false);
                    userRatingName = covertToResources(userRatingName);
                }

                text = "<html><table border='0'><tr>" +
                    "<td>" + Strings.message("panel.feeds.starz.recommendation") + "</td>" +
                    "<td><img src='" + name + "'></td></tr>" +
                    "<tr><td>" + Strings.message("panel.feeds.starz.your.rating") + "</td>" +
                    "<td>" + (userRatingSet ? "<img src='" + userRatingName + "'>"
                        : Strings.message("panel.feeds.starz.not.set")) +
                    "</td></tr></table></html>";
            } else if (comp == lbIcon && hoveredFeed.isDynamic())
            {
                DirectFeed dFeed = (DirectFeed)hoveredFeed;
                ReadingList[] readingLists = dFeed.getReadingLists();
                String[] names = new String[readingLists.length];
                for (int i = 0; i < readingLists.length; i++)
                {
                    ReadingList list = readingLists[i];
                    names[i] = list.getTitle();
                    if (names[i] == null) names[i] = list.getURL().toString();
                    names[i] += " (" + list.getParentGuide().getTitle() + ")";
                }
                text = MessageFormat.format(Strings.message("panel.feeds.readinglists"),
                    StringUtils.join(names, ","));
            } else if (comp == lbTitle)
            {
                text = hoveredFeed.getTitle();
            } else
            {
                if (hoveredFeed != null && hoveredFeed.isInvalid())
                {
                    text = MessageFormat.format(Strings.message("panel.feeds.error"),
                        hoveredFeed.getInvalidnessReason());
                }
            }

            return text;
        }

        private String covertToResources(String path)
        {
            if (path == null) return null;

            return path.startsWith(File.separator) ? "/" + path : path;
        }

        /**
         * Returns <code>TRUE</code> if the starz component is hovered.
         *
         * @param aPoint point in the coordinates of the cell.
         *
         * @return <code>TRUE</code> if hovered.
         */
        public boolean isStarzHovered(Point aPoint)
        {
            return lbStars.contains(aPoint);
        }
    }

    /**
     * Listens for  changes to render setting.
     */
    private class RenderSettingsChangeListener implements PropertyChangeListener
    {
        public void propertyChange(PropertyChangeEvent evt)
        {
            String prop = evt.getPropertyName();
            if (prop.equals(RenderingSettingsNames.THEME))
            {
                onListColorsUpdate();
            } else if (prop.equals(RenderingSettingsNames.IS_STARZ_SHOWING) ||
                    prop.equals(RenderingSettingsNames.IS_UNREAD_IN_FEEDS_SHOWING) ||
                    prop.equals(RenderingSettingsNames.IS_ACTIVITY_CHART_SHOWING))
            {
                updateFeedsListLayout();
            }
        }
    }

    /**
     * Simple feed selector.
     */
    private class SelectFeed implements Runnable
    {
        private final IFeed feed;

        public SelectFeed(IFeed aFeed)
        {
            feed = aFeed;
        }

        public void run()
        {
            selectListItem0(feed);
        }
    }
}

/**
* Takes care of handling selection gestures on the feeds list.
*/
class FeedSelectionListener extends MouseAdapter
    implements ListSelectionListener, PropertyChangeListener
{
    private java.util.Timer     timer;
    private FeedSelector        task;

    private final Object        eventLock;
    private volatile IFeed      eventFeed;
    private volatile long       eventTime;
    private volatile int        eventIndex;

    private boolean             feedSelectionDelayed;

    public FeedSelectionListener(long aFeedSelectionDelay)
    {
        eventLock = new Object();
        timer = new java.util.Timer(true);
        setFeedSelectionDelay(aFeedSelectionDelay);
    }

    // Sets the delay and reschedules the timer.
    private void setFeedSelectionDelay(long aDelay)
    {
        if (task != null) task.cancel();

        feedSelectionDelayed = (aDelay != 0);

        if (feedSelectionDelayed)
        {
            task = new FeedSelector(aDelay);
            timer.schedule(task, 1, aDelay);
        }
    }

    /**
     * Called when feed selection delay property changes.
     */
    public void propertyChange(PropertyChangeEvent evt)
    {
        Integer value = (Integer)evt.getNewValue();
        setFeedSelectionDelay(value.longValue());
    }

    /**
     * Call this whenever user clicks on one of the Channels in the ChannelList.
     *
     * @param e event object.
     */
    public void valueChanged(final ListSelectionEvent e)
    {
        // If either of these is true, the event can safely be ignored

        if (e.getValueIsAdjusting()) return;

        JList list = (JList)e.getSource();
        ListModel model = list.getModel();

        GlobalModel globalModel = GlobalModel.SINGLETON;
        IFeed prevFeed = globalModel == null ? null : globalModel.getSelectedFeed();
        int oldIndex = prevFeed == null ? -1 : ((GuideModel)model).indexOf(prevFeed);

        // Find out new selection index
        int selIndex = ListSelectionManager.evaluateSelectionIndex(list, oldIndex);

        final IFeed feed;
        feed = (selIndex == -1) ? null : (IFeed)model.getElementAt(selIndex);

        if (feedSelectionDelayed)
        {
            // We should always update selection event values for delayed selection
            // as we have invalid information of currently selected feed as the next
            // second it can become different.
            synchronized (eventLock)
            {
                eventTime = System.currentTimeMillis();
                eventFeed = feed;
                eventIndex = selIndex;
            }
        } else if (selIndex != oldIndex)
        {
            // We should update the feed directly only if the index has changed.
            selectFeed(feed);
        }
    }

    /**
     * Invoked when someone clicks over the feed in list.
     *
     * @param e event.
     */
    public void mousePressed(MouseEvent e)
    {
        // Every mouse press changes UI and every release sends event to the code.
        // We have to disarm a delay to avoid selection of the feed while the mouse
        // button is pressed. It causes problems when user quickly selects and deselect
        // the feed by doing press-release-press-... and at this moment feed becomes
        // selected again because of a triggered delayed feed selection as the result
        // of the first press, thereby frustrating the user.
        synchronized (eventLock)
        {
            Point point = e.getPoint();
            JList list = (JList)e.getSource();

            int index = list.locationToIndex(point);
            if (index != eventIndex)
            {
                eventIndex = -1;
                eventTime = -1;
                eventFeed = null;
            }
        }
    }

    /**
     * Select feed if it's not currently selected.
     *
     * @param feed feed to select.
     */
    protected void selectFeed(final IFeed feed)
    {
        if (feed != GlobalModel.SINGLETON.getSelectedFeed() && feed != null)
        {
            if (UifUtilities.isEDT())
            {
                GlobalController.SINGLETON.selectFeed(feed);
            } else
            {
                SwingUtilities.invokeLater(new Runnable()
                {
                    public void run()
                    {
                        GlobalController.SINGLETON.selectFeed(feed);
                    }
                });
            }
        }
    }

    /** Feed selector with delay. */
    private class FeedSelector extends TimerTask
    {
        private long lastProcessedTime;
        private long feedSelectionDelay;

        public FeedSelector(long aFeedSelectionDelay)
        {
            lastProcessedTime = 0;
            feedSelectionDelay = aFeedSelectionDelay;
        }

        /**
         * Called periodically to check if some feed should be selected.
         */
        public void run()
        {
            long time;
            IFeed feed;

            synchronized (eventLock)
            {
                time = eventTime;
                feed = eventFeed;
            }

            if (time > lastProcessedTime &&
                System.currentTimeMillis() - time > feedSelectionDelay)
            {
                selectFeed(feed);
                lastProcessedTime = time;
            }
        }
    }
}
TOP

Related Classes of com.salas.bb.views.mainframe.FeedsPanel$DraggingListListener

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.