001 /*
002 * This file is part of muCommander, http://www.mucommander.com
003 * Copyright (C) 2002-2008 Maxence Bernard
004 *
005 * muCommander is free software; you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation; either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * muCommander is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program. If not, see <http://www.gnu.org/licenses/>.
017 */
018
019 package com.mucommander.ui.icon;
020
021 import javax.swing.*;
022 import java.awt.*;
023 import java.awt.event.ActionEvent;
024 import java.awt.event.ActionListener;
025 import java.awt.geom.AffineTransform;
026 import java.lang.ref.WeakReference;
027 import java.util.HashSet;
028 import java.util.Iterator;
029
030 /**
031 * <code>javax.swing.Icon</code> implementation that manages animation.
032 * <p>
033 * This heavily borrows code from Technomage's <code>furbelow</code> package, distributed
034 * under the GNU Lesser General Public License.<br>
035 * The original source code can be found <a href="http://furbelow.svn.sourceforge.net/viewvc/furbelow/trunk/src/furbelow">here</a>.
036 * </p>
037 * @author twall, Nicolas Rinaudo
038 */
039 public abstract class AnimatedIcon implements Icon {
040 // - Default values ------------------------------------------------------------------
041 // -----------------------------------------------------------------------------------
042 /** Default number of frames per animation. */
043 public static final int DEFAULT_FRAME_COUNT = 8;
044 /** Default number of milliseconds between each frame. */
045 public static final int DEFAULT_FRAME_DELAY = 1000 / DEFAULT_FRAME_COUNT;
046
047
048
049 // - Instance fields -----------------------------------------------------------------
050 // -----------------------------------------------------------------------------------
051 /** All tracked components. */
052 private HashSet components = new HashSet();
053 /** Timer used to take the animation from one frame to the next. */
054 private Timer timer;
055 /** Index of the current frame. */
056 private int currentFrame;
057 /** Total number of frames in the animation. */
058 private int frameCount;
059 /** Whether or not the animation should be running. */
060 private boolean animate;
061
062
063
064 // - Initialisation ------------------------------------------------------------------
065 // -----------------------------------------------------------------------------------
066 /**
067 * Creates a new animated icon.
068 * <p>
069 * This is a convenience constructor and is strictly equivalent to calling
070 * <code>{@link #AnimatedIcon(int,int)}({@link #DEFAULT_FRAME_COUNT}, {@link #DEFAULT_FRAME_DELAY});</code>
071 * </p>
072 */
073 public AnimatedIcon() {this(DEFAULT_FRAME_COUNT, DEFAULT_FRAME_DELAY);}
074
075 /**
076 * Creates a new animated icon with the specified number of frames.
077 * <p>
078 * This is a convenience constructor and is strictly equivalent to calling
079 * <code>{@link #AnimatedIcon(int,int)}(frameCount, {@link #DEFAULT_FRAME_DELAY});</code>
080 * </p>
081 * @param frameCount number of frames in the animation.
082 */
083 public AnimatedIcon(int frameCount) {this(frameCount, DEFAULT_FRAME_DELAY);}
084
085 /**
086 * Creates a new animated icon with the specified number of frames and repaint delay.
087 * @param frameCount number of frames in the animation.
088 * @param repaintDelay number of milliseconds to sleep between each frame.
089 */
090 public AnimatedIcon(int frameCount, int repaintDelay) {
091 // Initialises the animation timer.
092 timer = new Timer(repaintDelay, new AnimationUpdater(this));
093 timer.setRepeats(true);
094
095 // Initialises frame control.
096 setFrameCount(frameCount);
097 setFrameDelay(repaintDelay);
098 }
099
100
101
102 // - Abstract methods ----------------------------------------------------------------
103 // -----------------------------------------------------------------------------------
104 /**
105 * Returns the icon's width.
106 * @return the icon's width.
107 */
108 public abstract int getIconWidth();
109
110 /**
111 * Returns the icon's height.
112 * @return the icon's height.
113 */
114 public abstract int getIconHeight();
115
116 /**
117 * Paints the current frame.
118 * @param c component in which the frame is being painted.
119 * @param g graphics in which to paint the frame.
120 * @param x horizontal coordinate at which to paint the frame.
121 * @param y vertical coordinate at which to paint the frame.
122 */
123 protected abstract void paintFrame(Component c, Graphics g, int x, int y);
124
125
126
127 // - Frame management ----------------------------------------------------------------
128 // -----------------------------------------------------------------------------------
129 /**
130 * Sets the total number of frames in the animation.
131 * @param count total number of frames in the animation.
132 */
133 public synchronized void setFrameCount(int count) {this.frameCount = count;}
134
135 /**
136 * Returns the total number of frames in the animation.
137 * @return the total number of frames in the animation.
138 */
139 public synchronized int getFrameCount() {return frameCount;}
140
141 /**
142 * Returns the index of the current frame in the animation.
143 * @return the index of the current frame in the animation.
144 */
145 public synchronized int getFrame() {return currentFrame;}
146
147 /**
148 * Sets the index of the current frame in the animation.
149 * <p>
150 * If the method does actually change the current frame, it will trigger a repaint.
151 * </p>
152 * @param frame index of the current frame in the animation.
153 */
154 public synchronized void setFrame(int frame) {
155 if(frame != currentFrame) {
156 if(frame == 0)
157 currentFrame = 0;
158 else
159 currentFrame = frame % frameCount;
160 repaint();
161 }
162 }
163
164 /**
165 * Takes the animation to its next frame.
166 * <p>
167 * This is a convenience method and is strictly equivalent to calling
168 * <code>{@link #setFrame(int) setFrame}({@link #getFrame() getFrame()} + 1)</code>.
169 * </p>
170 */
171 public synchronized void nextFrame() {setFrame(currentFrame + 1);}
172
173 /**
174 * Sets the number of milliseconds the animation will sleep between each frame.
175 * <p>
176 * If set to 0, the animation will stop.
177 * </p>
178 * @param delay number of milliseconds the animation will sleep between each frame.
179 */
180 public synchronized void setFrameDelay(int delay) {timer.setDelay(delay);}
181
182 /**
183 * Starts / stops the animation.
184 * @param a whether the animation should be started or stopped.
185 */
186 public synchronized void setAnimated(boolean a) {
187 // Starts the animation if necessary.
188 if(a) {
189 if(!timer.isRunning())
190 timer.restart();
191 }
192
193 // Stops the animation if necessary.
194 else if(timer.isRunning())
195 timer.stop();
196 animate = a;
197 }
198
199 /**
200 * Returns <code>true</code> if the animation is currently running.
201 * <p>
202 * Note that this method will return <code>true</code> if the animation is <b>meant</b> to be running,
203 * for example if the icon is not visible but would be animated if it was.
204 * </p>
205 * @return <code>true</code> if the animation is currently running, <code>false</code>.
206 */
207 public synchronized boolean isAnimated() {return animate;}
208
209 /**
210 * Returns the number of milliseconds the animation will sleep between each frame.
211 * @return the number of milliseconds the animation will sleep between each frame.
212 */
213 public synchronized int getFrameDelay() {return timer.getDelay();}
214
215
216
217 // - Painting ------------------------------------------------------------------------
218 // -----------------------------------------------------------------------------------
219 /**
220 * Paints the icon's current frame.
221 * @param c component in which to paint the icon.
222 * @param g graphic context in which to paint the icon.
223 * @param x horizontal coordinate at which to paint the icon.
224 * @param y vertical coordinate at which to paint the icon.
225 */
226 public synchronized void paintIcon(Component c, Graphics g, int x, int y) {
227 // Paints the current frame.
228 paintFrame(c, g, x, y);
229
230 // Stores the component and starts / restarts the timer if necessary.
231 if(c != null) {
232 AffineTransform transform;
233
234 transform = ((Graphics2D)g).getTransform();
235 components.add(new TrackedComponent(c, x, y, (int)(getIconWidth() * transform.getScaleX()), (int)(getIconHeight() * transform.getScaleY())));
236
237 // Restarts the timer if necessary.
238 if(!timer.isRunning() && animate)
239 timer.restart();
240 }
241 }
242
243 /**
244 * Forces the icon to repaint.
245 */
246 protected synchronized void repaint() {
247 Iterator iterator;
248
249 // If the component list is empty, we can stop the timer.
250 if(components.isEmpty())
251 timer.stop();
252
253 // Repaints all pending components.
254 else {
255 iterator = components.iterator();
256 while(iterator.hasNext())
257 ((TrackedComponent)iterator.next()).repaint();
258 components.clear();
259 }
260 }
261
262 protected void finalize() throws Throwable {
263 // Forces the timer to stop when the animation isn't used anymore.
264 timer.stop();
265
266 super.finalize();
267 }
268
269
270
271 // - Container tracking --------------------------------------------------------------
272 // -----------------------------------------------------------------------------------
273 /**
274 * Used to keep track of the various components in which an animated icon is being painted.
275 * @author twall, Nicolas Rinaudo
276 */
277 private static class TrackedComponent {
278 /** Component in which the icon must be painted. */
279 private Component component;
280 /** Horizontal coordinate at which the icon should be painted. */
281 private int x;
282 /** Vertical coordinate at which the icon should be painted. */
283 private int y;
284 /** Width of the icon (used for clipping). */
285 private int width;
286 /** Height of the icon (used for clipping). */
287 private int height;
288 /** Component's hashcode. */
289 private int hashCode;
290
291 /**
292 * Creates a new tracked component.
293 * @param c component in which to paint the icon.
294 * @param x horizontal coordinate at which to paint the icon.
295 * @param y vertical coordinate at which to paint the icon.
296 * @param width width of the icon.
297 * @param height height of the icon.
298 */
299 public TrackedComponent(Component c, int x, int y, int width, int height) {
300 Component ancestor;
301
302 // Identifies the component that displays the icon.
303 if((ancestor = findNonRendererAncestor(c)) != c) {
304 Point pt = SwingUtilities.convertPoint(c, x, y, ancestor);
305 c = ancestor;
306 x = pt.x;
307 y = pt.y;
308 }
309
310 // Stores all the necessary information and computes the tracked component's hashcode.
311 component = c;
312 this.x = x;
313 this.y = y;
314 this.width = width;
315 this.height = height;
316 hashCode = (x + "," + y + ":" + c.hashCode()).hashCode();
317 }
318
319 public int hashCode() {return hashCode;}
320
321 /**
322 * Finds the specified component's first non-renderer ancestor.
323 * @param c component whose ancestors should be explored.
324 */
325 private Component findNonRendererAncestor(Component c) {
326 Component ancestor;
327
328 ancestor = SwingUtilities.getAncestorOfClass(CellRendererPane.class, c);
329 if (ancestor != null && ancestor != c && ancestor.getParent() != null)
330 c = findNonRendererAncestor(ancestor.getParent());
331 return c;
332 }
333
334 /**
335 * Forces the tracked component to repaint the animated icon.
336 */
337 public void repaint() {component.repaint(x, y, width, height);}
338 }
339
340
341
342 // - Timer management ----------------------------------------------------------------
343 // -----------------------------------------------------------------------------------
344 /**
345 * Receives timer events and notifies the icon.
346 * @author twall, Nicolas Rinaudo
347 */
348 private static class AnimationUpdater implements ActionListener {
349 /** Weak reference to the animation. */
350 private WeakReference icon;
351
352 /**
353 * Creates a new animation updater on the specified icon.
354 * @param icon animation to update.
355 */
356 public AnimationUpdater(AnimatedIcon icon) {this.icon = new WeakReference(icon);}
357
358 /**
359 * Notifies the icon that it should update.
360 * @param event ignored.
361 */
362 public void actionPerformed(ActionEvent event) {
363 AnimatedIcon i;
364
365 // Makes sure the animation hasn't been garbage collected.
366 if((i = (AnimatedIcon)icon.get()) != null)
367 i.nextFrame();
368 }
369 }
370 }