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