// 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: UnreadButton.java,v 1.19 2007/09/17 12:14:11 spyromus Exp $
//
package com.salas.bb.views.mainframe;
import com.jgoodies.uif.util.ResourceUtils;
import com.salas.bb.utils.i18n.Strings;
import com.salas.bb.utils.uif.UifUtilities;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.text.MessageFormat;
/**
* UnreadButton is a combination label/button for showing the article unread count. It is drawn like
* a flat label, but displays as a button when the mouse rolls over it. Note this derives from
* JLabel, not JButton, and has its own implementation for button pressing. (By not using JButton,
* we have a little more flexibility in the button appearance, and avoid some complications in
* ensuring correct display when the overall look-and-feel is changed.)
*
*/
public class UnreadButton extends JLabel
{
/**
* Font to draw button text in. (Beware of default font name, which
* differs between Java 1.4 and 1.5.)
*/
private static Font myFont = new Font("SansSerif", Font.BOLD, 9);
/** ToolTip template for single unread item. */
private String toolTipTemplateSingle;
/** ToolTip template for multiple unread read items. */
private String toolTipTemplateMany;
/** Max #digits we display in unread count. */
private static final int MAX_DISPLAY_DIGITS = 3;
/** Button state has not been initalized. */
private static final int BUTTON_STATE_UNSET = -1;
/** Normal state: hasn't been moused on/over. */
private static final int BUTTON_STATE_PLAIN = 0;
/** Mouse over button - button raised. */
private static final int BUTTON_STATE_RAISED = 1;
/** Mouse pressed, mouse still tracking inside button. */
private static final int BUTTON_STATE_PRESSED_ON = 2;
/** Mouse pressed, mouse tracking outside button. */
private static final int BUTTON_STATE_PRESSED_OFF = 3;
/** How to draw button in normal state. */
private static DrawSpecs drawSpecsPlain = new DrawSpecs(
"unreadPlain.icon", // button icon
Color.BLACK, // text color
null); // text shadow color
/** How to draw button in raised state. */
private static DrawSpecs drawSpecsRaised = new DrawSpecs(
"unreadRaised.icon", // button icon
Color.WHITE, // text color
new Color(253, 101, 102)); // text shadow color
/** How to draw button in pressed state. */
private static DrawSpecs drawSpecsPressed = new DrawSpecs(
"unreadPressed.icon", // button icon
new Color(240, 179, 179), // text color
new Color(219, 72, 72)); // text shadow color
/** Size we want to be.*/
private static final Dimension MY_SIZE = new Dimension(
drawSpecsPlain.getImageIcon().getIconWidth() + 1,
drawSpecsPlain.getImageIcon().getIconHeight() + 1);
/** To show or not to show the menu on mouse click. */
private static volatile boolean showMenuOnClick = true;
/** Current button state, one of <code>BUTTON_STATE_XXX</code>.*/
private int myButtonState = BUTTON_STATE_UNSET;
/** Count of unread articles. */
private int myUnreadCount;
/** The object this button is attached to. */
private Object attachedToObject;
/**
* Construct an UnreadButton.
*/
public UnreadButton()
{
setFocusable(false);
setHorizontalAlignment(SwingConstants.RIGHT);
setVerticalAlignment(SwingConstants.TOP);
initSize();
setIconTextGap(0);
setIcon(new ButtonIcon());
setOpaque(false);
setHorizontalTextPosition(SwingConstants.CENTER);
enableEvents(AWTEvent.MOUSE_EVENT_MASK);
enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);
setButtonState(BUTTON_STATE_PLAIN);
}
/**
* Sets the on-click menu state.
*
* @param value <code>TRUE</code> to show the menu, otherwise -- to invoke action directly.
*/
public static void setShowMenuOnClick(boolean value)
{
showMenuOnClick = value;
}
/**
* Set our button to draw one of these states
* Normal: mouse not over button
* Raised: mouse over button; button is shown raised
* Pressed On: button pressed down
* Pressed Off: button pressed but mouse tracked off.
* @param state
* One of the BUTTON_STATE_XXX constants
*/
protected void setButtonState(int state)
{
if (state != myButtonState)
{
myButtonState = state;
repaint();
}
}
/**
* Get our draw state for button.
* @return current button state
*/
protected int getButtonState()
{
return myButtonState;
}
/**
* Initialize control.
* @param unreadCount number of unread items
*/
public void init(int unreadCount)
{
update(unreadCount);
setFont(myFont);
setButtonState(BUTTON_STATE_PLAIN);
}
/**
* Update the unread count for the control. Like <code>init</code>
* except that the existing mouse state is preserved.
* @param unreadCount number of unread items
*/
public void update(int unreadCount)
{
myUnreadCount = unreadCount;
repaint();
setEnabled(myUnreadCount > 0);
setVisible(myUnreadCount > 0);
initToolTip();
}
/**
* Set up the tool tip message.
* @param templateSingle
* The message text for a single unread item. Must be suitable for passing to
* <code>MessageFormat.format</code> with one arg {0} representing the unread arg
* count.
* @param templateMany
* The message text for multiple unread items. Must be suitable for passing to
* <code>MessageFormat.format</code> with one arg {0} representing the unread arg
* count.
*/
public void initToolTipMessage(String templateSingle, String templateMany)
{
toolTipTemplateSingle = templateSingle;
toolTipTemplateMany = templateMany;
initToolTip();
}
/**
* Set up our tooltip. Report number of unread articles, and presence of keyword hit if any.
*
*/
private void initToolTip()
{
String tip = null;
if (myUnreadCount > 0 && toolTipTemplateSingle != null)
{
Integer[] args = { myUnreadCount };
tip = MessageFormat.format((myUnreadCount == 1)
? toolTipTemplateSingle : toolTipTemplateMany, args);
}
setToolTipText(tip);
}
/**
* Set up our size.
*/
protected void initSize()
{
setSize(MY_SIZE);
setMinimumSize(MY_SIZE);
setMaximumSize(MY_SIZE);
setPreferredSize(MY_SIZE);
}
/**
* Adds an <code>ActionListener</code> to the button.
* @param l the <code>ActionListener</code> to be added
*/
public void addActionListener(ActionListener l)
{
listenerList.add(ActionListener.class, l);
}
/**
* Removes an <code>ActionListener</code> from the button.
*
* @param l the listener to be removed
*/
public void removeActionListener(ActionListener l)
{
listenerList.remove(ActionListener.class, l);
}
/**
* Get the list of action listeners.
* @return array of action listeners
*/
public ActionListener[] getActionListeners()
{
return listenerList.getListeners(ActionListener.class);
}
/**
* Inform the action listeners that the button has been clicked.
*
* @param source source of the event (object this button was attached to).
*/
protected void invokeAction(Object source)
{
ActionEvent action = new ActionEvent(source, 0, "markRead");
ActionListener[] listeners = getActionListeners();
for (ActionListener listener : listeners) listener.actionPerformed(action);
}
/**
* Records the object this button is attached to.
*
* @param object an object.
*/
public void setAttachedToObject(Object object)
{
attachedToObject = object;
}
/***
* ButtonIcon provides the image for our button.
* All state is driven from the parent class.
*/
class ButtonIcon implements Icon
{
/**
* Our height is the JLabel's height.
* @see javax.swing.Icon#getIconHeight()
*/
public int getIconHeight()
{
return MY_SIZE.height;
}
/**
* Our width is the JLabel's width.
* @see javax.swing.Icon#getIconWidth()
*/
public int getIconWidth()
{
return MY_SIZE.width;
}
/**
* Paint ourself in our current state.
* @param c component we paint in
* @param g graphics context
* @param x x coord to paint in
* @param y y coord to paint in
* @see javax.swing.Icon#paintIcon(java.awt.Component, java.awt.Graphics, int, int)
*/
public void paintIcon(Component c, Graphics g, int x, int y)
{
DrawSpecs drawSpecs;
switch (getButtonState())
{
case BUTTON_STATE_PLAIN:
drawSpecs = drawSpecsPlain;
break;
case BUTTON_STATE_RAISED:
case BUTTON_STATE_PRESSED_OFF:
drawSpecs = drawSpecsRaised;
break;
case BUTTON_STATE_PRESSED_ON:
drawSpecs = drawSpecsPressed;
break;
default:
return;
}
ImageIcon imageIcon = drawSpecs.getImageIcon();
g.setColor(Color.WHITE);
g.drawImage(imageIcon.getImage(), 0, 0, imageIcon.getIconWidth(),
imageIcon.getIconHeight(), UnreadButton.this);
g.setFont(myFont);
String text = Integer.toString(myUnreadCount);
if (text.length() > MAX_DISPLAY_DIGITS) text = "...";
FontMetrics fm = g.getFontMetrics(myFont);
Rectangle2D r = fm.getStringBounds(text, g);
int base = fm.getAscent();
// center text
int dx = (int)Math.round((MY_SIZE.width - 1 - r.getWidth()) / 2);
int dy = (int)(base + Math.round((MY_SIZE.height - 1 - r.getHeight()) / 2));
// tweak for proper layout
// --dy;
Color shadowColor = drawSpecs.getShadowColor();
Color textColor = drawSpecs.getTextColor();
if (shadowColor != null)
{
g.setColor(shadowColor);
g.drawString(text, dx + 1, dy);
}
g.setColor(textColor);
g.drawString(text, dx, dy);
}
}
/**
* Process mouse clicks, entries and exits.
*
* @param e event.
*/
protected void processMouseEvent(MouseEvent e)
{
super.processMouseEvent(e);
switch (e.getID())
{
case MouseEvent.MOUSE_PRESSED:
// Display pushed-in button if we're enabled.
if (isEnabled())
{
setButtonState(BUTTON_STATE_PRESSED_ON);
} else
{
UifUtilities.delegateEventToParent(this, e);
}
break;
case MouseEvent.MOUSE_RELEASED:
// If mouse has been released and is still tracking over us,
// revert button display and invoke the action.
if (isEnabled())
{
setButtonState(BUTTON_STATE_PLAIN);
Rectangle r = getBounds();
r.setLocation(0, 0);
if (r.contains(e.getPoint()))
{
final Object source = attachedToObject;
if (showMenuOnClick)
{
// Show popup menu with confirmation item
JPopupMenu menu = new JPopupMenu();
JMenuItem item = new JMenuItem(Strings.message("unreadbutton.mark.as.read"));
item.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
invokeAction(source);
}
});
menu.add(item);
menu.show(this, e.getPoint().x, e.getPoint().y);
} else invokeAction(source);
}
} else
{
UifUtilities.delegateEventToParent(this, e);
}
break;
case MouseEvent.MOUSE_CLICKED:
UifUtilities.delegateEventToParent(this, e);
break;
case MouseEvent.MOUSE_ENTERED:
// When mouse enters our bounds, display raised button. If had been pressed
// previously and then tracked outside our bounds, revert to presed state
// if mouse re-enters our bounds.
if (isEnabled())
{
switch (getButtonState())
{
case BUTTON_STATE_PLAIN:
setButtonState(BUTTON_STATE_RAISED);
break;
case BUTTON_STATE_PRESSED_OFF:
setButtonState(BUTTON_STATE_PRESSED_ON);
break;
default:
break;
}
}
break;
case MouseEvent.MOUSE_EXITED:
// When mouse moves out, normally revert to normal button state (i.e. button not
// displayed). But if user has pressed mouse and tracked outside our bounds, then
// display in raised state.
if (isEnabled())
{
// If user clicks on button and then drags out of bounds,
// button stays pressed.
switch (getButtonState())
{
case BUTTON_STATE_RAISED:
setButtonState(BUTTON_STATE_PLAIN);
break;
case BUTTON_STATE_PRESSED_ON:
setButtonState(BUTTON_STATE_PRESSED_OFF);
break;
default:
break;
}
}
break;
default:
break;
}
}
/**
* Delegate handling of all motion events to the parent.
*
* @param e event.
*/
protected void processMouseMotionEvent(MouseEvent e)
{
UifUtilities.delegateEventToParent(this, e);
}
/**
* DrawSpecs describes the icon and colors used to draw the button
* in one of its states.
*/
static class DrawSpecs
{
/** Icon for drawing button in this state. */
private final ImageIcon imageIcon;
/** Color to draw text. */
private final Color textColor;
/** Color to draw text shadow, or null if none. */
private final Color shadowColor;
/**
* @param iconName Name of the icon used to draw button background.
* @param text Color of the text.
* @param shadow Color of the text shadow.
*/
DrawSpecs(String iconName, Color text, Color shadow)
{
imageIcon = ResourceUtils.getIcon(iconName);
textColor = text;
shadowColor = shadow;
}
/**
* @return Icon for button background.
*/
ImageIcon getImageIcon()
{
return imageIcon;
}
/**
* @return Color to draw text.
*/
Color getTextColor()
{
return textColor;
}
/**
* @return Color to draw text shadow.
*/
Color getShadowColor()
{
return shadowColor;
}
}
}