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 (p instanceof InputStreamImageSource) {
082 Method m = InputStreamImageSource.class.getDeclaredMethod("getDecoder", null);
083 m.setAccessible(true);
084 ImageDecoder d = (ImageDecoder)m.invoke(p, null);
085 if (d 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 (o 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 }
|