001: package abbot.editor.widgets;
002:
003: import java.awt.Component;
004: import java.awt.Graphics;
005: import java.awt.Graphics2D;
006: import java.awt.Image;
007: import java.awt.Point;
008: import java.awt.geom.AffineTransform;
009: import java.awt.image.ImageObserver;
010: import java.awt.image.ImageProducer;
011: import java.io.IOException;
012: import java.io.InputStream;
013: import java.lang.ref.WeakReference;
014: import java.lang.reflect.Field;
015: import java.lang.reflect.Method;
016: import java.net.MalformedURLException;
017: import java.net.URL;
018: import java.util.HashSet;
019: import java.util.Iterator;
020: import java.util.Map;
021: import java.util.Set;
022: import java.util.WeakHashMap;
023: import javax.swing.CellRendererPane;
024: import javax.swing.Icon;
025: import javax.swing.ImageIcon;
026: import javax.swing.SwingUtilities;
027: import sun.awt.image.GifImageDecoder;
028: import sun.awt.image.ImageDecoder;
029: import sun.awt.image.InputStreamImageSource;
030: import abbot.Log;
031:
032: /** Ensures animated icons are properly handled within objects that use
033: * renderers within a {@link CellRendererPane} to render the icon. Keeps
034: * a list of repaint rectangles to be used to queue repaint requests when
035: * the animated icon indicates an update. The set of repaint rectangles
036: * is cleared after the repaint requests are queued.
037: * @author twall
038: */
039: public class AnimatedIcon implements Icon {
040:
041: /** Cache results to reduce decoding overhead. */
042: private static Map decoded = new WeakHashMap();
043:
044: /** Returns whether the given icon is an animated GIF. */
045: public static boolean isAnimated(Icon icon) {
046: if (icon instanceof ImageIcon) {
047: Image image = ((ImageIcon) icon).getImage();
048: if (image != null) {
049: // Quick check for commonly-occurring animated GIF comment
050: Object comment = image.getProperty("comment", null);
051: if (String.valueOf(comment).startsWith("GifBuilder"))
052: return true;
053:
054: // Check cache of already-decoded images
055: if (decoded.containsKey(image)) {
056: return Boolean.TRUE.equals(decoded.get(image));
057: }
058:
059: InputStream is = null;
060: try {
061: URL url = new URL(icon.toString());
062: is = url.openConnection().getInputStream();
063: } catch (MalformedURLException e) {
064: // Not from a URL, ignore it
065: } catch (IOException e) {
066: Log.warn("Failed to load from " + icon, e);
067: }
068: if (is == null) {
069: ImageProducer p = image.getSource();
070: try {
071: // Beware: lots of hackery to obtain the image input stream
072: // Be sure to catch security exceptions
073: if (p instanceof InputStreamImageSource) {
074: Method m = InputStreamImageSource.class
075: .getDeclaredMethod("getDecoder",
076: null);
077: m.setAccessible(true);
078: ImageDecoder d = (ImageDecoder) m.invoke(p,
079: null);
080: if (d instanceof GifImageDecoder) {
081: GifImageDecoder gd = (GifImageDecoder) d;
082: Field input = ImageDecoder.class
083: .getDeclaredField("input");
084: input.setAccessible(true);
085: is = (InputStream) input.get(gd);
086: }
087: }
088: } catch (Exception e) {
089: Log.warn("Can't decode from image producer: "
090: + p, e);
091: }
092: }
093: if (is != null) {
094: GifDecoder decoder = new GifDecoder();
095: decoder.read(is);
096: boolean animated = decoder.getFrameCount() > 1;
097: decoded.put(image, Boolean.valueOf(animated));
098: return animated;
099: }
100: }
101: return false;
102: }
103: return icon instanceof AnimatedIcon;
104: }
105:
106: private ImageIcon original;
107: private Set repaints = new HashSet();
108:
109: /** For use by derived classes that don't have an original. */
110: protected AnimatedIcon() {
111: }
112:
113: /** Create an icon that takes care of animating itself on components
114: * which use a CellRendererPane.
115: */
116: public AnimatedIcon(ImageIcon original) {
117: this .original = original;
118: new AnimationObserver(this , original);
119: }
120:
121: /** Trigger a repaint on all components on which we've previously been
122: * painted.
123: */
124: protected synchronized void repaint() {
125: for (Iterator i = repaints.iterator(); i.hasNext();) {
126: ((RepaintArea) i.next()).repaint();
127: }
128: repaints.clear();
129: }
130:
131: public int getIconHeight() {
132: return original.getIconHeight();
133: }
134:
135: public int getIconWidth() {
136: return original.getIconWidth();
137: }
138:
139: public synchronized void paintIcon(Component c, Graphics g, int x,
140: int y) {
141: paintFrame(c, g, x, y);
142: if (c != null) {
143: int w = getIconWidth();
144: int h = getIconHeight();
145: AffineTransform tx = ((Graphics2D) g).getTransform();
146: w = (int) (w * tx.getScaleX());
147: h = (int) (h * tx.getScaleY());
148: registerRepaintArea(c, x, y, w, h);
149: }
150: }
151:
152: protected void paintFrame(Component c, Graphics g, int x, int y) {
153: original.paintIcon(c, g, x, y);
154: }
155:
156: /** Register repaint areas, which get get cleared once the repaint request
157: * has been queued.
158: */
159: protected void registerRepaintArea(Component c, int x, int y,
160: int w, int h) {
161: repaints.add(new RepaintArea(c, x, y, w, h));
162: }
163:
164: /** Object to encapsulate an area on a component to be repainted. */
165: private class RepaintArea {
166: public int x, y, w, h;
167: public Component component;
168: private int hashCode;
169:
170: public RepaintArea(Component c, int x, int y, int w, int h) {
171: Component ancestor = findNonRendererAncestor(c);
172: if (ancestor != c) {
173: Point pt = SwingUtilities.convertPoint(c, x, y,
174: ancestor);
175: c = ancestor;
176: x = pt.x;
177: y = pt.y;
178: }
179: this .component = c;
180: this .x = x;
181: this .y = y;
182: this .w = w;
183: this .h = h;
184: String hash = String.valueOf(x) + "," + y + ":"
185: + c.hashCode();
186: this .hashCode = hash.hashCode();
187: }
188:
189: /** Find the first ancestor <em>not</em> descending from a
190: * {@link CellRendererPane}.
191: */
192: private Component findNonRendererAncestor(Component c) {
193: Component ancestor = SwingUtilities.getAncestorOfClass(
194: CellRendererPane.class, c);
195: if (ancestor != null && ancestor != c
196: && ancestor.getParent() != null) {
197: c = findNonRendererAncestor(ancestor.getParent());
198: }
199: return c;
200: }
201:
202: /** Queue a repaint request for this area. */
203: public void repaint() {
204: component.repaint(x, y, w, h);
205: }
206:
207: public boolean equals(Object o) {
208: if (o instanceof RepaintArea) {
209: RepaintArea area = (RepaintArea) o;
210: return area.component == component && area.x == x
211: && area.y == y && area.w == w && area.h == h;
212: }
213: return false;
214: }
215:
216: /** Since we're using a HashSet. */
217: public int hashCode() {
218: return hashCode;
219: }
220:
221: public String toString() {
222: return "Repaint(" + component.getClass().getName() + "@"
223: + x + "," + y + " " + w + "x" + h + ")";
224: }
225: }
226:
227: /** Detect changes in the original animated image, and remove self
228: * if the target icon is GC'd.
229: * @author twall
230: */
231: private static class AnimationObserver implements ImageObserver {
232: private WeakReference ref;
233: private ImageIcon original;
234:
235: public AnimationObserver(AnimatedIcon animIcon,
236: ImageIcon original) {
237: this .original = original;
238: this .original.setImageObserver(this );
239: ref = new WeakReference(animIcon);
240: }
241:
242: /** Queue repaint requests for all known painted areas. */
243: public boolean imageUpdate(Image img, int flags, int x, int y,
244: int width, int height) {
245: if ((flags & (FRAMEBITS | ALLBITS)) != 0) {
246: AnimatedIcon animIcon = (AnimatedIcon) ref.get();
247: if (animIcon != null) {
248: animIcon.repaint();
249: } else
250: original.setImageObserver(null);
251: }
252: // Return true if we want to keep painting
253: return (flags & (ALLBITS | ABORT)) == 0;
254: }
255: }
256: }
|