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    }