// 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: AsyncImagePanel.java,v 1.20 2007/11/19 08:24:48 spyromus Exp $
//
package com.salas.bb.views.feeds.image;
import com.salas.bb.utils.i18n.Strings;
import com.salas.bb.utils.uif.images.ImageFetcher;
import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.image.ImageObserver;
import java.net.URL;
/**
* Image icon with asynchronous load of data.
*/
class AsyncImagePanel extends JComponent
{
private static final int INDICATION_BORDER = 0;
private static final int INDICATION_FADE = 1;
private static final int INDICATION_FRAME = 2;
private static final int INDICATION_TYPE = INDICATION_FRAME;
private static final int STATUS_INCEPTION = -1;
private static final int STATUS_LOADING = 0;
private static final int STATUS_LOADED = 1;
private static final int STATUS_FAILED = 2;
private static final AlphaComposite COMPOSITE_MODE_1 =
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f);
private static final AlphaComposite COMPOSITE_MODE_2 =
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f);
private final URL imageURL;
private final ImageHandler observer;
private int maxWidth;
private int maxHeight;
private Image image;
private Image rescaledImage;
private Image badge;
private int status;
private boolean secondMode;
private boolean overrideFirstMode;
/**
* Creates component.
*
* @param aImageURL URL to load data from.
* @param aWidth max width of image.
* @param aHeight max height of image.
* @param aBorder border to wrap picture with.
* @param aSecondMode <code>TRUE</code> to switch to second mode.
*
* @throws NullPointerException if URL is not specified.
*/
public AsyncImagePanel(URL aImageURL, int aWidth, int aHeight, Border aBorder,
boolean aSecondMode)
{
if (aImageURL == null) throw new NullPointerException(Strings.error("unspecified.url"));
imageURL = aImageURL;
image = null;
rescaledImage = null;
observer = new ImageHandler();
secondMode = aSecondMode;
overrideFirstMode = false;
status = STATUS_INCEPTION;
setBorder(aBorder);
setImageSize(new Dimension(aWidth, aHeight));
}
/**
* Switches display into second mode.
*
* @param aSecondMode second mode.
*/
public void setSecondMode(boolean aSecondMode)
{
if (secondMode != aSecondMode)
{
secondMode = aSecondMode;
repaint();
}
}
/**
* Sets a badge to paint over the image.
*
* @param badge badge to paint.
*/
public void setBadge(Image badge)
{
this.badge = badge;
repaint();
}
/**
* Sets new image size and repaints everything.
*
* @param dim dimension.
*/
public void setImageSize(Dimension dim)
{
maxWidth = dim.width;
maxHeight = dim.height;
int width = maxWidth;
int height = maxHeight;
Border border = getBorder();
if (border != null)
{
Insets borderInsets = border.getBorderInsets(this);
width += borderInsets.left + borderInsets.right;
height += borderInsets.top + borderInsets.bottom;
}
setPreferredSize(new Dimension(width, height));
if (STATUS_LOADED == status)
{
rescaledImage = null;
loadImage();
}
}
/**
* Starts loading image.
*/
private void loadImage()
{
status = STATUS_LOADING;
image = ImageFetcher.load(imageURL);
if (image.getHeight(observer) != -1) status = STATUS_LOADED;
}
/**
* Calls the UI delegate's paint method, if the UI delegate is non-<code>null</code>. We pass
* the delegate a copy of the <code>Graphics</code> object to protect the rest of the paint code
* from irrevocable changes (for example, <code>Graphics.translate</code>). <p> If you override
* this in a subclass you should not make permanent changes to the passed in
* <code>Graphics</code>. For example, you should not alter the clip <code>Rectangle</code> or
* modify the transform. If you need to do these operations you may find it easier to create a
* new <code>Graphics</code> from the passed in <code>Graphics</code> and manipulate it.
* Further, if you do not invoker super's implementation you must honor the opaque property,
* that is if this component is opaque, you must completely fill in the background in a
* non-opaque color. If you do not honor the opaque property you will likely see visual
* artifacts.
*
* @param g the <code>Graphics</code> object to protect
*
* @see #paint
* @see javax.swing.plaf.ComponentUI
*/
protected void paintComponent(Graphics g)
{
if (status == STATUS_INCEPTION) loadImage();
if (INDICATION_TYPE == INDICATION_FADE && g instanceof Graphics2D)
{
((Graphics2D)g).setComposite(!overrideFirstMode && secondMode
? COMPOSITE_MODE_2 : COMPOSITE_MODE_1);
}
switch (status)
{
case STATUS_LOADING:
paintLoading(g);
break;
case STATUS_LOADED:
paintLoaded(g);
break;
default:
paintFailed(g);
break;
}
}
/** Paint loading state. */
private void paintLoading(Graphics g)
{
g.setColor(Color.LIGHT_GRAY);
paintPlaceholder(g);
}
/** Paint failed state. */
private void paintFailed(Graphics g)
{
g.setColor(Color.GRAY);
paintPlaceholder(g);
}
/** Paint image placeholder. */
private void paintPlaceholder(Graphics g)
{
Border border = getBorder();
Insets insets = border.getBorderInsets(this);
int il = insets.left;
int it = insets.top;
g.fillRect(il, it, maxWidth, maxHeight);
getBorder().paintBorder(this, g, il, it, maxWidth, maxHeight);
int imgWidth = maxWidth;
int imgHeight = maxHeight;
paintBadge(g, imgWidth, imgHeight);
}
/**
* Paints badge when present.
*
* @param g graphics context.
* @param imgWidth image width.
* @param imgHeight image height.
*/
private void paintBadge(Graphics g, int imgWidth, int imgHeight)
{
if (badge != null)
{
int bwidth = badge.getWidth(null);
int bheight = badge.getHeight(null);
if (bwidth > 0 && bheight > 0 && imgWidth > bwidth && imgHeight > bheight)
{
Border border = getBorder();
Insets insets = border.getBorderInsets(this);
// Badge is valid
int x = maxWidth / 2 + imgWidth / 2 - bwidth - 2;
int y = maxHeight / 2 + imgHeight / 2 - bheight - 2;
g.drawImage(badge, insets.left + x, insets.top + y, bwidth, bheight, null);
}
}
}
/** Paint loaded state. */
private void paintLoaded(Graphics g)
{
if (rescaledImage == null)
{
rescaledImage = rescaleImage(image);
if (rescaledImage != null) image = null;
}
if (rescaledImage != null)
{
int width = rescaledImage.getWidth(observer);
int height = rescaledImage.getHeight(observer);
if (width == -1 || height == -1)
{
paintLoading(g);
} else
{
Border border = getBorder();
Insets insets = border.getBorderInsets(this);
int picX = (maxWidth - width) / 2 + insets.left;
int picY = (maxHeight - height) / 2 + insets.top;
g.drawImage(rescaledImage, picX, picY, width, height, observer);
if (INDICATION_TYPE == INDICATION_BORDER && g instanceof Graphics2D)
{
((Graphics2D)g).setComposite(!overrideFirstMode && secondMode
? COMPOSITE_MODE_2 : COMPOSITE_MODE_1);
}
border.paintBorder(this, g, picX, picY, width, height);
if (INDICATION_TYPE == INDICATION_FRAME && !secondMode)
{
g.setColor(Color.WHITE);
g.drawRect(picX + 1, picY + 1, width - 2, height - 2);
g.setColor(Color.BLACK);
g.drawRect(picX, picY, width, height);
}
paintBadge(g, width, height);
}
}
}
/**
* Rescales an image to fit the desired dimensions.
*
* @param src source image.
*
* @return destination image.
*/
private Image rescaleImage(Image src)
{
Image dest = null;
int imgWidth = src.getWidth(observer);
if (imgWidth != -1)
{
int imgHeight = src.getHeight(observer);
if (imgHeight != -1)
{
int picWidth = imgWidth;
int picHeight = imgHeight;
if (picWidth > maxWidth || picHeight > maxHeight)
{
picWidth = imgWidth > imgHeight ? maxWidth : (imgWidth * maxHeight / imgHeight);
picHeight = imgWidth > imgHeight ? (imgHeight * maxWidth / imgWidth) : maxHeight;
}
dest = src.getScaledInstance(picWidth, picHeight, Image.SCALE_SMOOTH);
}
}
return dest;
}
/**
* Paints the component's border. <p> If you override this in a subclass you should not make
* permanent changes to the passed in <code>Graphics</code>. For example, you should not alter
* the clip <code>Rectangle</code> or modify the transform. If you need to do these operations
* you may find it easier to create a new <code>Graphics</code> from the passed in
* <code>Graphics</code> and manipulate it.
*
* @param g the <code>Graphics</code> context in which to paint
*
* @see #paint
* @see #setBorder
*/
protected void paintBorder(Graphics g)
{
// No border painting required.
}
/**
* Catch mouse pointer over the image and repaint it with normal mode.
*
* @param e mouse event.
*/
protected void processMouseEvent(MouseEvent e)
{
super.processMouseEvent(e);
switch (e.getID())
{
case MouseEvent.MOUSE_ENTERED:
overrideFirstMode = true;
repaint();
break;
case MouseEvent.MOUSE_EXITED:
overrideFirstMode = false;
repaint();
break;
default:
break;
}
}
/**
* Keeps an eye on loading of images.
*/
private class ImageHandler implements ImageObserver
{
/**
* Invoked when loading of image brought some new bits or has been finished.
*
* @param img the image being observed.
* @param infoflags the bitwise inclusive OR of the following flags: <code>WIDTH</code>,
* <code>HEIGHT</code>, <code>PROPERTIES</code>, <code>SOMEBITS</code>,
* <code>FRAMEBITS</code>, <code>ALLBITS</code>, <code>ERROR</code>,
* <code>ABORT</code>.
* @param x the <i>x</i> coordinate.
* @param y the <i>y</i> coordinate.
* @param width the width.
* @param height the height.
*
* @return <code>false</code> if the infoflags indicate that the image is completely loaded;
* <code>true</code> otherwise.
*/
public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height)
{
boolean loaded = (infoflags & (ALLBITS | ERROR | ABORT)) != 0;
if (loaded)
{
status = (infoflags & ALLBITS) != 0 ? STATUS_LOADED : STATUS_FAILED;
repaint(0);
}
return !loaded;
}
}
}