// 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: ResultsList.java,v 1.14 2007/09/19 16:09:25 spyromus Exp $
//
package com.salas.bb.search;
import com.salas.bb.core.GlobalController;
import com.salas.bb.domain.IArticle;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.List;
/**
* Search list, which is capable of displaying result items in a grouped, sorted and
* filtered way.
*/
public class ResultsList extends JPanel implements IResultsListModelListener, Scrollable
{
/** Any dates allowed. */
public static final int DATE_ANY = 0;
/** Today items only allowed. */
public static final int DATE_TODAY = 1;
/** Items since yesterday are allowed. */
public static final int DATE_YESTERDAY = 2;
/** Items during this week are allowed. */
public static final int DATE_WEEK = 3;
/** Items during this month are allowed. */
public static final int DATE_MONTH = 4;
/** Items during this year are allowed. */
public static final int DATE_YEAR = 5;
/** The map of group keys to groups. */
private final Map<Integer, ResultGroupPanel> groups = new TreeMap<Integer, ResultGroupPanel>();
/** Current date filtering option. */
private int dateRange = DATE_ANY;
/** Limit time is based on the {@link #dateRange} property and works as filter. */
private long limitTime;
/** Number of items to show in each group initially. */
private int itemGroupLimit;
/** Currently selected item. */
private ResultItemPanel selectedItem;
/** Action listeners. */
private final List<ActionListener> listeners = new ArrayList<ActionListener>();
/**
* Creates search results list.
*
* @param model model this list will be using.
*/
public ResultsList(ResultsListModel model)
{
// Register the model
model.addListener(this);
itemGroupLimit = 5;
limitTime = getLimitTime(dateRange);
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
setFocusable(true);
enableEvents(AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK);
}
/**
* Invoked when the model is cleared.
*
* @param model model.
*/
public void onClear(IResultsListModel model)
{
groups.clear();
removeAll();
revalidate();
}
/**
* Invoked when an item is added to the model.
*
* @param model model.
* @param item item added.
* @param group group the item was added to.
*/
public void onItemAdded(IResultsListModel model, ResultItem item, ResultGroup group)
{
// Create the item panel and register it
ResultItemPanel itemPanel = new ResultItemPanel(item);
ResultGroupPanel groupPanel = groups.get(group.getKey());
// Register the item within the panel
int index = groupPanel.register(itemPanel) + 1;
index += indexOf(groupPanel);
add(itemPanel, index);
// Revalidate the list
revalidate();
}
/**
* Invoked when an item is removed from the group.
*
* @param model model.
* @param item item removed.
* @param group group the item was removed from.
*/
public void onItemRemoved(IResultsListModel model, ResultItem item, ResultGroup group)
{
// Remove item panel from the group
ResultGroupPanel groupPanel = groups.get(group.getKey());
ResultItemPanel itemPanel = groupPanel.removeItemPanelFor(item);
if (itemPanel != null) remove(itemPanel);
// Revalidate the list
revalidate();
}
/**
* Invoked when the model adds a group to hold new items.
*
* @param model model.
* @param group group added.
* @param ordered when <code>TRUE</code> group is added in the order of appearance.
*/
public void onGroupAdded(IResultsListModel model, ResultGroup group, boolean ordered)
{
// Create the group panel and initialize it with parameters
ResultGroupPanel groupPanel = createGroupPanel(group);
groupPanel.setItemLimit(itemGroupLimit);
groupPanel.setLimitTime(limitTime);
groups.put(group.getKey(), groupPanel);
// Find a group before which to add this one
ResultGroupPanel preGroup = null;
if (!ordered)
{
Integer key = group.getKey();
Iterator<Integer> it = groups.keySet().iterator();
while (preGroup == null && it.hasNext())
{
Object otherKey = it.next();
if (key == otherKey && it.hasNext()) preGroup = groups.get(it.next());
}
}
// Add the group and its more-component
int index = preGroup == null ? -1 : indexOf(preGroup);
add(groupPanel, index);
add(groupPanel.getMoreComponent(), index == -1 ? -1 : index + 1);
}
/**
* Creates group panel.
*
* @param group group.
*
* @return panel.
*/
protected ResultGroupPanel createGroupPanel(ResultGroup group)
{
return new ResultGroupPanel(group.getName());
}
/**
* Invoked when the model removes a group to hold new items.
*
* @param model model.
* @param group group added.
*/
public void onGroupRemoved(IResultsListModel model, ResultGroup group)
{
// Remove the panel and it's more-component
ResultGroupPanel panel = groups.remove(group.getKey());
remove(panel);
remove(panel.getMoreComponent());
// Remove all items
panel.removeAllItemsFrom(this);
revalidate();
}
/**
* Invoked when the model changes a group to hold new items.
*
* @param model model.
* @param group group added.
*/
public void onGroupUpdated(IResultsListModel model, ResultGroup group)
{
// Update the group title
ResultGroupPanel groupPanel = groups.get(group.getKey());
if (groupPanel != null)
{
String title = group.getName();
groupPanel.setTitle(title);
}
}
/**
* Sets the number of item to display in the group max.
*
* @param limit limit.
*/
public void setItemGroupLimit(int limit)
{
itemGroupLimit = limit;
for (ResultGroupPanel group : groups.values()) group.setItemLimit(limit);
}
/**
* Sets date range for filtering out unwanted items.
*
* @param aDateRange range.
*/
public void setDateRange(int aDateRange)
{
if (dateRange != aDateRange)
{
dateRange = aDateRange;
limitTime = getLimitTime(dateRange);
for (ResultGroupPanel group : groups.values()) group.setLimitTime(limitTime);
}
}
/**
* Adds action listener.
*
* @param l listener.
*/
public void addActionListener(ActionListener l)
{
listeners.add(l);
}
/**
* Removes action listeners.
*
* @param l listener.
*/
public void removeActionListener(ActionListener l)
{
listeners.remove(l);
}
/**
* Returns currently selected item.
*
* @return item.
*/
public ResultItem getSelectedItem()
{
return selectedItem == null ? null : selectedItem.getItem();
}
// ---------------------------------------------------------------------------------------------
/**
* Returns index of component.
*
* @param comp component.
*
* @return index.
*/
private int indexOf(Component comp)
{
int index = -1;
for (int i = 0; index == -1 && i < getComponentCount(); i++)
{
if (getComponent(i) == comp) index = i;
}
return index;
}
// ---------------------------------------------------------------------------------------------
/**
* Invoked when new item is selected.
*
* @param item item.
*/
private void onItemSelected(ResultItemPanel item)
{
requestFocusInWindow();
if (selectedItem != null) selectedItem.setSelected(false);
selectedItem = item;
if (selectedItem != null)
{
selectedItem.setSelected(true);
scrollRectToVisible(selectedItem.getBounds());
}
}
/**
* Invoked when item selection is confirmed (fired).
*/
void onItemFired()
{
ActionEvent event = null;
for (ActionListener listener : listeners)
{
if (event == null) event = new ActionEvent(this, 0, "");
listener.actionPerformed(event);
}
}
/**
* Invoked when item selection is used to toggle the read state.
*/
private void onItemToggleReadState()
{
if (selectedItem != null)
{
Object o = selectedItem.getItem().getObject();
if (o instanceof IArticle)
{
IArticle article = (IArticle)o;
GlobalController.readArticles(!article.isRead(),
null,
null,
article);
}
}
}
/**
* Invoked when item selection is used to toggle the pin state.
*/
private void onItemTogglePinState()
{
if (selectedItem != null)
{
Object o = selectedItem.getItem().getObject();
if (o instanceof IArticle)
{
IArticle article = (IArticle)o;
GlobalController.pinArticles(!article.isPinned(),
null,
null,
article);
}
}
}
/**
* Invoked when previous visible item selection is requested.
*/
public void onPrevItemSelected()
{
if (selectedItem != null)
{
int index = indexOf(selectedItem);
ResultItemPanel toSelect = null;
for (int i = index - 1; toSelect == null && i > 0; i--)
{
Component c = getComponent(i);
if (c instanceof ResultItemPanel && c.isVisible()) toSelect = (ResultItemPanel)c;
}
if (toSelect != null) onItemSelected(toSelect);
}
}
/**
* Invoked when next visible item selection is requested.
*/
public void onNextItemSelected()
{
int index = selectedItem == null ? 0 : indexOf(selectedItem);
ResultItemPanel toSelect = null;
for (int i = index + 1; toSelect == null && i < getComponentCount(); i++)
{
Component c = getComponent(i);
if (c instanceof ResultItemPanel && c.isVisible()) toSelect = (ResultItemPanel)c;
}
if (toSelect != null) onItemSelected(toSelect);
}
/**
* Calculates limit time basing on the date range.
*
* @param dateRange date range.
*
* @return limit time.
*/
private static long getLimitTime(int dateRange)
{
if (dateRange == DATE_ANY) return -1L;
// Get today time
GregorianCalendar cal = new GregorianCalendar();
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
long time;
switch (dateRange)
{
case DATE_YESTERDAY:
cal.add(Calendar.DATE, -1);
break;
case DATE_WEEK:
cal.add(Calendar.DATE, -7);
break;
case DATE_MONTH:
cal.set(Calendar.DATE, 1);
break;
case DATE_YEAR:
cal.set(Calendar.DAY_OF_YEAR, 1);
break;
default:
break;
}
time = cal.getTimeInMillis();
return time;
}
// ---------------------------------------------------------------------------------------------
/**
* Returns the preferred size of the viewport for a view component. For example, the preferred size of a
* <code>JList</code> component is the size required to accommodate all of the cells in its list. However, the value
* of <code>preferredScrollableViewportSize</code> is the size required for <code>JList.getVisibleRowCount</code>
* rows. A component without any properties that would affect the viewport size should just return
* <code>getPreferredSize</code> here.
*
* @return the preferredSize of a <code>JViewport</code> whose view is this <code>Scrollable</code>
*
* @see javax.swing.JViewport#getPreferredSize
*/
public Dimension getPreferredScrollableViewportSize()
{
return getPreferredSize();
}
/**
* Components that display logical rows or columns should compute the scroll increment that will completely expose
* one new row or column, depending on the value of orientation. Ideally, components should handle a partially
* exposed row or column by returning the distance required to completely expose the item.
* <p/>
* Scrolling containers, like JScrollPane, will use this method each time the user requests a unit scroll.
*
* @param visibleRect The view area visible within the viewport
* @param orientation Either SwingConstants.VERTICAL or SwingConstants.HORIZONTAL.
* @param direction Less than zero to scroll up/left, greater than zero for down/right.
*
* @return The "unit" increment for scrolling in the specified direction. This value should always be positive.
*
* @see javax.swing.JScrollBar#setUnitIncrement
*/
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction)
{
int increment = 1;
if (orientation == SwingConstants.VERTICAL)
{
int px = visibleRect.x;
int py = visibleRect.y;
increment = getIncrementForBorderChild(direction, px, py);
if (increment == 0)
{
// Nowhere to move for the border child
// See if we can move one child up / down
if (direction < 0)
{
// up
if (py > 0) increment = getIncrementForBorderChild(direction, px, py - 1);
} else
{
// down
int height = getHeight();
if (py < height - visibleRect.height) increment = getIncrementForBorderChild(direction, px, py + 1);
}
// We need to compensate for these '-1' or '+1' above
if (increment > 0) increment++;
}
}
return increment;
}
private int getIncrementForBorderChild(int direction, int px, int py)
{
int increment = 0;
Component comp = findComponentAt(px, py);
if (comp != null)
{
Point inCompPoint = SwingUtilities.convertPoint(this, px, py, comp);
int y = inCompPoint.y;
if (direction < 0)
{
// up
if (y > 0) increment = y;
} else
{
// down
int height = comp.getHeight();
if (y < height) increment = height - y;
}
}
return increment;
}
/**
* Components that display logical rows or columns should compute the scroll increment that will completely expose
* one block of rows or columns, depending on the value of orientation.
* <p/>
* Scrolling containers, like JScrollPane, will use this method each time the user requests a block scroll.
*
* @param visibleRect The view area visible within the viewport
* @param orientation Either SwingConstants.VERTICAL or SwingConstants.HORIZONTAL.
* @param direction Less than zero to scroll up/left, greater than zero for down/right.
*
* @return The "block" increment for scrolling in the specified direction. This value should always be positive.
*
* @see javax.swing.JScrollBar#setBlockIncrement
*/
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction)
{
int px = visibleRect.x;
int py = visibleRect.y;
int he = getHeight();
int maxy = he - visibleRect.height;
Component started = findComponentAt(px, py);
int inc = 0;
boolean found = false;
if (direction > 0)
{
for (int y = py + 1; !found && y < maxy; y++)
{
Component comp = findComponentAt(px, y);
if (comp != started && comp instanceof ResultGroupPanel)
{
found = true;
inc = getIncrementForBorderChild(-direction, px, y) + (y - py) - 2;
} else
{
// Jump to the end of the item
y += getIncrementForBorderChild(direction, px, y);
}
}
} else
{
for (int y = py - 1; !found && y >= 0; y--)
{
Component comp = findComponentAt(px, y);
if (comp != started && comp instanceof ResultGroupPanel)
{
found = true;
inc = getIncrementForBorderChild(direction, px, y) + (py - y);
} else
{
// Jump to the end of the item
y -= getIncrementForBorderChild(direction, px, y);
}
}
}
return inc;
}
/**
* Return true if a viewport should always force the width of this <code>Scrollable</code> to match the width of the
* viewport. For example a normal text view that supported line wrapping would return true here, since it would be
* undesirable for wrapped lines to disappear beyond the right edge of the viewport. Note that returning true for a
* Scrollable whose ancestor is a JScrollPane effectively disables horizontal scrolling.
* <p/>
* Scrolling containers, like JViewport, will use this method each time they are validated.
*
* @return True if a viewport should force the Scrollables width to match its own.
*/
public boolean getScrollableTracksViewportWidth()
{
return true;
}
/**
* Return true if a viewport should always force the height of this Scrollable to match the height of the viewport.
* For example a columnar text view that flowed text in left to right columns could effectively disable vertical
* scrolling by returning true here.
* <p/>
* Scrolling containers, like JViewport, will use this method each time they are validated.
*
* @return True if a viewport should force the Scrollables height to match its own.
*/
public boolean getScrollableTracksViewportHeight()
{
return false;
}
// ---------------------------------------------------------------------------------------------
/**
* Processes mouse events.
*
* @param e event.
*/
protected void processMouseEvent(MouseEvent e)
{
if (!SwingUtilities.isLeftMouseButton(e)) return;
Component c = getComponentAt(e.getPoint());
if (!(c instanceof ResultItemPanel)) return;
switch (e.getID())
{
case MouseEvent.MOUSE_PRESSED:
onItemSelected((ResultItemPanel)c);
break;
case MouseEvent.MOUSE_CLICKED:
if (e.getClickCount() == 2) onItemFired();
break;
}
}
/**
* Processes navigation events.
*
* @param e event.
*/
protected void processKeyEvent(KeyEvent e)
{
if (e.getID() != KeyEvent.KEY_PRESSED) return;
int code = e.getKeyCode();
switch (code)
{
case KeyEvent.VK_UP:
onPrevItemSelected();
break;
case KeyEvent.VK_DOWN:
onNextItemSelected();
break;
case KeyEvent.VK_ENTER:
onItemFired();
break;
case KeyEvent.VK_Q:
onItemToggleReadState();
break;
case KeyEvent.VK_P:
onItemTogglePinState();
break;
case KeyEvent.VK_PAGE_UP:
Rectangle visible = getVisibleRect();
visible.y = Math.max(0, visible.y - visible.height);
scrollRectToVisible(visible);
break;
case KeyEvent.VK_PAGE_DOWN:
visible = getVisibleRect();
visible.y = Math.max(0, Math.min(getHeight() - visible.height, visible.y + visible.height));
scrollRectToVisible(visible);
break;
case KeyEvent.VK_HOME:
visible = getVisibleRect();
visible.y = 0;
scrollRectToVisible(visible);
break;
case KeyEvent.VK_END:
visible = getVisibleRect();
visible.y = Math.max(0, getHeight() - visible.height);
scrollRectToVisible(visible);
break;
}
}
}