// 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: ArticleActivityMeter.java,v 1.14 2006/05/31 12:49:31 spyromus Exp $
//
package com.salas.bb.views.mainframe;
import com.salas.bb.utils.uif.UifUtilities;
import com.salas.bb.utils.i18n.Strings;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
/**
* Article activity meter component.
*/
public class ArticleActivityMeter extends JComponent
{
private static final int NUM_DAYS = 7;
private static final int NUM_BLOCKS_PER_DAY = 6;
/** Size of each drawn square, including gap. */
private static final int BLOCK_SIZE = 4;
/** Width of gap. */
private static final int BLOCK_GAP = 1;
/** Size of overflow triangle at top. */
private static final int OVER_SIZE = 4;
/** Border around all blocks. */
private static final int BORDER_WIDTH = 1;
private static final Color ARTICLE_READ_COLOR = new Color(84, 194, 84); //113, 113, 255);
private static final Color ARTICLE_UNREAD_COLOR = new Color(194, 84, 84); //217, 0, 0);
private static final Color NO_ARTICLE_COLOR = new Color(194, 194, 194); //229, 229, 229);
private static final Color ARTICLE_HOVER_HIGHLIGHT = new Color(230, 230, 230); //0, 255, 0);
/** If <code>TRUE</code>, newest days are shown on right side. */
private static final boolean SHOW_NEWEST_ON_RIGHT = true;
/** Our fixed size. */
private static final Dimension MY_SIZE =
new Dimension(NUM_DAYS * BLOCK_SIZE + BORDER_WIDTH * 2,
NUM_BLOCKS_PER_DAY * BLOCK_SIZE + OVER_SIZE + BORDER_WIDTH * 2);
/** Day mouse hovering over; -1 for none. */
private int myHoverDay = -1;
/** Article statistics we display. */
private UnreadStats myUnreadStats;
/**
* Constructs ArticleActivityMeter, used to show
* last N days of read/unread articles.
*/
public ArticleActivityMeter()
{
setFocusable(false);
}
/**
* Process mouse clicks, entries and exits.
*
* @param e event.
*/
protected void processMouseEvent(MouseEvent e)
{
super.processMouseEvent(e);
switch (e.getID())
{
case MouseEvent.MOUSE_EXITED:
// Clear the highlight when the mouse leaves
setHoverDay(-1);
break;
case MouseEvent.MOUSE_PRESSED:
case MouseEvent.MOUSE_RELEASED:
case MouseEvent.MOUSE_CLICKED:
// We need this to enable parent to activate dragging when user clicks over
// the this component.
UifUtilities.delegateEventToParent(this, e);
break;
default:
break;
}
}
/**
* Process mouse movement event and delegate dragging to the parent.
*
* @param e event.
*/
protected void processMouseMotionEvent(MouseEvent e)
{
super.processMouseMotionEvent(e);
// Track mouse motion in order to highlight the day's blocks
// under the mouse.
if (e.getID() == MouseEvent.MOUSE_MOVED)
{
setHoverDay(pointToDay(e.getPoint()));
} else if (e.getID() == MouseEvent.MOUSE_DRAGGED)
{
// We need this to let the parent handle dragging of feeds correctly.
UifUtilities.delegateEventToParent(this, e);
}
}
/**
* Initializes control for display.
*
* @param stats unread statistics to display.
*/
public void init(UnreadStats stats)
{
myUnreadStats = stats;
setForeground(Color.DARK_GRAY);
setSize(MY_SIZE);
setPreferredSize(MY_SIZE);
initToolTip();
}
/**
* Highlight the particular day, for mouse tracking purposes.
*
* @param day index of the day to highlight in range [0; MAX_DAYS) or
* <code>-1</code> to clear highlight.
*/
public void setHoverDay(int day)
{
if (day != myHoverDay)
{
myHoverDay = day;
initToolTip();
repaint();
}
}
/**
* Set the tooltip corresponding to whatever day is hovered over,
* or no tooltip if nothing being hovered on.
*/
protected void initToolTip()
{
String toolTip;
if (myHoverDay < 0)
{
// Using an empty string here as opposed to a null keeps us registered with the
// tooltip manager, which ensures that the tooltip appears even when only
// once mouse move occurs over the component.
toolTip = "";
} else
{
Object[] args = new Object[3];
String dayString, message;
// Identify day of week: "today", "yesterday", or "last X".
UnreadStats.DayCount dayCount = myUnreadStats.getDayCount(myHoverDay);
if (myHoverDay < 2)
{
if (myHoverDay == 0)
{
dayString = Strings.message("activitymeter.today");
} else
{
dayString = Strings.message("activitymeter.yesterday");
}
} else
{
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, -1 * (myHoverDay));
SimpleDateFormat dateFormat = new SimpleDateFormat("'" + Strings.message("activitymeter.last") +
" 'EEEE");
dayString = dateFormat.format(cal.getTime());
}
int total = dayCount.getTotal();
int read = dayCount.getRead();
int unread = dayCount.getUnread();
args[0] = dayString;
args[1] = new Integer(total);
args[2] = new Integer(unread);
// Choose a message corresponding to # of read/unread articles.
if (total == 0)
{
message = Strings.message("activitymeter.no.articles");
} else if (total == 1)
{
message = (unread == 1)
? Strings.message("activitymeter.1.article.unread")
: Strings.message("activitymeter.1.article");
} else if (read == 0)
{
message = Strings.message("activitymeter.n.articles.all.unread");
} else if (unread == 0)
{
message = Strings.message("activitymeter.n.articles");
} else
{
message = Strings.message("activitymeter.n.articles.m.unread");
}
toolTip = MessageFormat.format(message, args);
}
setToolTipText(toolTip);
}
/**
* Paints the activity meter, with or without the mouse hover highlight.
* The drawing is not particularly efficient: each of the 35
* blocks and 5 overflow indicators is drawn individually.
* It's likely this could be improved by creating cached images
* for larger chunks of the display, so that we can paint
* with fewer operations. Some candidates:
* - create a cached image of 7x5 "no-article" blocks, and then draw
* the read/unread article blocks on top where necessary.
* - create cached images for columns of 5 read and 5 unread blocks, since
* those often commonly appear.
*
* @see javax.swing.JComponent#paintComponent(java.awt.Graphics)
*/
protected void paintComponent(Graphics g)
{
// Draw the green hover highlight background
// if we are in hover mode
if (myHoverDay >= 0)
{
int x = dayToX(myHoverDay) - BLOCK_GAP;
g.setColor(ARTICLE_HOVER_HIGHLIGHT);
g.fillRect(x, 0, BLOCK_SIZE + BLOCK_GAP,
BLOCK_SIZE * NUM_BLOCKS_PER_DAY + OVER_SIZE + BORDER_WIDTH * 2);
}
// Draw days from left to right.
for (int day = 0; day < NUM_DAYS; ++day)
{
UnreadStats.DayCount dc = myUnreadStats.getDayCount(day);
int read, unread;
unread = dc.getUnread();
read = unread + dc.getRead();
// Draw blocks for this day from bottom to top.
// init coords to just below bottom-most block for this day
int bx;
if (SHOW_NEWEST_ON_RIGHT)
{
bx = dayToX(day);
} else
{
bx = day * BLOCK_SIZE + BORDER_WIDTH;
}
int by = OVER_SIZE + (NUM_BLOCKS_PER_DAY * BLOCK_SIZE) + BORDER_WIDTH;
for (int j = 0; j < NUM_BLOCKS_PER_DAY + 1; ++j)
{
Color col;
if (j < unread)
{
col = ARTICLE_UNREAD_COLOR;
} else if (j < read)
{
col = ARTICLE_READ_COLOR;
} else
{
col = NO_ARTICLE_COLOR;
}
if (day == myHoverDay) col = col.darker();
g.setColor(col);
if (j < NUM_BLOCKS_PER_DAY)
{
// draw the normal block
by -= BLOCK_SIZE;
g.fillRect(bx, by, BLOCK_SIZE - BLOCK_GAP, BLOCK_SIZE - BLOCK_GAP);
} else //if (j < read)
{
// Draw an overflow triangle, if the overflow
// corresponds to an unread or read value.
by -= BLOCK_GAP + 1;
g.fillRect(bx, by, BLOCK_SIZE - BLOCK_GAP, 1);
by--;
g.fillRect(bx + 1, by, BLOCK_SIZE - BLOCK_GAP - 1, 1);
by--;
g.fillRect(bx + 2, by, BLOCK_SIZE - BLOCK_GAP - 2, 1);
}
}
}
}
/**
* Finds the x coordinate of block drawn on a given day.
*
* @param day 0 - today, 1 - yesterday, etc.
*
* @return x-coordinate of right edge of block.
*/
static int dayToX(int day)
{
int x;
if (SHOW_NEWEST_ON_RIGHT)
{
x = MY_SIZE.width - ((day + 1) * BLOCK_SIZE) - BORDER_WIDTH;
} else
{
x = day * BLOCK_SIZE + BORDER_WIDTH;
}
return x;
}
/**
* Given a point in our coordinates, returns the number of the day
* it corresponds to.
*
* @param p the point.
*
* @return the day (0 == today, 1 == yesterday, etc.)
*/
static int pointToDay(Point p)
{
int n;
if (SHOW_NEWEST_ON_RIGHT)
{
n = (MY_SIZE.width - BORDER_WIDTH - p.x) / BLOCK_SIZE;
} else
{
n = (p.x - BORDER_WIDTH) / BLOCK_SIZE;
}
n = Math.min(n, NUM_DAYS - 1);
n = Math.max(n, 0);
return n;
}
}