/* Copyright (c) 2006-2007 Timothy Wall, All Rights Reserved
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* <p/>
* This library 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
* Lesser General Public License for more details.
*/
package org.rr.commons.swing.icon;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.geom.AffineTransform;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import javax.swing.CellRendererPane;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.SwingUtilities;
import org.rr.commons.swing.image.GifDecoder;
import sun.awt.image.GifImageDecoder;
import sun.awt.image.ImageDecoder;
import sun.awt.image.InputStreamImageSource;
/**
* Taken from the furbelow project. http://sourceforge.net/projects/furbelow
*
* Ensures animated icons are properly handled within objects that use
* renderers within a {@link CellRendererPane} to render the icon. Keeps
* a list of repaint rectangles to be used to queue repaint requests when
* the animated icon indicates an update. The set of repaint rectangles
* is cleared after the repaint requests are queued.
* @author twall
*/
public class AnimatedIcon implements Icon {
/** Cache results to reduce decoding overhead. */
private static Map decoded = new WeakHashMap();
/** Returns whether the given icon is an animated GIF. */
public static boolean isAnimated(Icon icon) {
if (icon instanceof ImageIcon) {
Image image = ((ImageIcon)icon).getImage();
if (image != null) {
// Quick check for commonly-occurring animated GIF comment
Object comment = image.getProperty("comment", null);
if (String.valueOf(comment).startsWith("GifBuilder"))
return true;
// Check cache of already-decoded images
if (decoded.containsKey(image)) {
return Boolean.TRUE.equals(decoded.get(image));
}
InputStream is = null;
try {
URL url = new URL(icon.toString());
is = url.openConnection().getInputStream();
}
catch(Exception e) {
e.printStackTrace();
}
if (is == null) {
try {
// Beware: lots of hackery to obtain the image input stream
// Be sure to catch security exceptions
ImageProducer p = image.getSource();
if (p instanceof InputStreamImageSource) {
Method m = InputStreamImageSource.class.getDeclaredMethod("getDecoder", null);
m.setAccessible(true);
ImageDecoder d = (ImageDecoder)m.invoke(p, null);
if (d instanceof GifImageDecoder) {
GifImageDecoder gd = (GifImageDecoder)d;
Field input = ImageDecoder.class.getDeclaredField("input");
input.setAccessible(true);
is = (InputStream)input.get(gd);
}
}
}
catch(Exception e) {
e.printStackTrace();
}
}
if (is != null) {
GifDecoder decoder = new GifDecoder();
decoder.read(is);
boolean animated = decoder.getFrameCount() > 1;
decoded.put(image, Boolean.valueOf(animated));
return animated;
}
}
return false;
}
return icon instanceof AnimatedIcon;
}
private ImageIcon original;
private Set repaints = new HashSet();
/** For use by derived classes that don't have an original. */
protected AnimatedIcon() { }
/** Create an icon that takes care of animating itself on components
* which use a CellRendererPane.
*/
public AnimatedIcon(ImageIcon original) {
this.original = original;
new AnimationObserver(this, original);
}
/** Trigger a repaint on all components on which we've previously been
* painted.
*/
protected synchronized void repaint() {
for (Iterator i=repaints.iterator();i.hasNext();) {
((RepaintArea)i.next()).repaint();
}
repaints.clear();
}
public int getIconHeight() {
return original.getIconHeight();
}
public int getIconWidth() {
return original.getIconWidth();
}
public synchronized void paintIcon(Component c, Graphics g, int x, int y) {
paintFrame(c, g, x, y);
if (c != null) {
int w = getIconWidth();
int h = getIconHeight();
AffineTransform tx = ((Graphics2D)g).getTransform();
w = (int)(w * tx.getScaleX());
h = (int)(h * tx.getScaleY());
registerRepaintArea(c, x, y, w, h);
}
}
protected void paintFrame(Component c, Graphics g, int x, int y) {
original.paintIcon(c, g, x, y);
}
/** Register repaint areas, which get get cleared once the repaint request
* has been queued.
*/
protected void registerRepaintArea(Component c, int x, int y, int w, int h) {
repaints.add(new RepaintArea(c, x, y, w, h));
}
/** Object to encapsulate an area on a component to be repainted. */
private class RepaintArea {
public int x, y, w, h;
public Component component;
private int hashCode;
public RepaintArea(Component c, int x, int y, int w, int h) {
Component ancestor = findNonRendererAncestor(c);
if (ancestor != c) {
Point pt = SwingUtilities.convertPoint(c, x, y, ancestor);
c = ancestor;
x = pt.x;
y = pt.y;
}
this.component = c;
this.x = x;
this.y = y;
this.w = w;
this.h = h;
String hash = String.valueOf(x) + "," + y + ":" + c.hashCode();
this.hashCode = hash.hashCode();
}
/** Find the first ancestor <em>not</em> descending from a
* {@link CellRendererPane}.
*/
private Component findNonRendererAncestor(Component c) {
Component ancestor = SwingUtilities.getAncestorOfClass(CellRendererPane.class, c);
if (ancestor != null && ancestor != c && ancestor.getParent() != null) {
c = findNonRendererAncestor(ancestor.getParent());
}
return c;
}
/** Queue a repaint request for this area. */
public void repaint() {
component.repaint(x, y, w, h);
}
public boolean equals(Object o) {
if (o instanceof RepaintArea) {
RepaintArea area = (RepaintArea)o;
return area.component == component
&& area.x == x && area.y == y
&& area.w == w && area.h == h;
}
return false;
}
/** Since we're using a HashSet. */
public int hashCode() {
return hashCode;
}
public String toString() {
return "Repaint(" + component.getClass().getName() + "@" + x + "," + y + " " + w + "x" + h + ")";
}
}
/** Detect changes in the original animated image, and remove self
* if the target icon is GC'd.
* @author twall
*/
private static class AnimationObserver implements ImageObserver {
private WeakReference ref;
private ImageIcon original;
public AnimationObserver(AnimatedIcon animIcon, ImageIcon original) {
this.original = original;
this.original.setImageObserver(this);
ref = new WeakReference(animIcon);
}
/** Queue repaint requests for all known painted areas. */
public boolean imageUpdate(Image img, int flags, int x, int y, int width, int height) {
if ((flags & (FRAMEBITS|ALLBITS)) != 0) {
AnimatedIcon animIcon = (AnimatedIcon)ref.get();
if (animIcon != null) {
animIcon.repaint();
}
else
original.setImageObserver(null);
}
// Return true if we want to keep painting
return (flags & (ALLBITS|ABORT)) == 0;
}
}
}