/* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.ui.icon; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.AffineTransform; import java.lang.ref.WeakReference; import java.util.HashSet; /** * <code>javax.swing.Icon</code> implementation that manages animation. * <p> * This heavily borrows code from Technomage's <code>furbelow</code> package, distributed * under the GNU Lesser General Public License.<br> * The original source code can be found <a href="http://furbelow.svn.sourceforge.net/viewvc/furbelow/trunk/src/furbelow">here</a>. * </p> * @author twall, Nicolas Rinaudo */ public abstract class AnimatedIcon implements Icon { // - Default values ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** Default number of frames per animation. */ public static final int DEFAULT_FRAME_COUNT = 8; /** Default number of milliseconds between each frame. */ public static final int DEFAULT_FRAME_DELAY = 1000 / DEFAULT_FRAME_COUNT; // - Instance fields ----------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** All tracked components. */ private HashSet<TrackedComponent> components = new HashSet<TrackedComponent>(); /** Timer used to take the animation from one frame to the next. */ private Timer timer; /** Index of the current frame. */ private int currentFrame; /** Total number of frames in the animation. */ private int frameCount; /** Whether or not the animation should be running. */ private boolean animate; // - Initialisation ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** * Creates a new animated icon. * <p> * This is a convenience constructor and is strictly equivalent to calling * <code>{@link #AnimatedIcon(int,int)}({@link #DEFAULT_FRAME_COUNT}, {@link #DEFAULT_FRAME_DELAY});</code> * </p> */ public AnimatedIcon() {this(DEFAULT_FRAME_COUNT, DEFAULT_FRAME_DELAY);} /** * Creates a new animated icon with the specified number of frames. * <p> * This is a convenience constructor and is strictly equivalent to calling * <code>{@link #AnimatedIcon(int,int)}(frameCount, {@link #DEFAULT_FRAME_DELAY});</code> * </p> * @param frameCount number of frames in the animation. */ public AnimatedIcon(int frameCount) {this(frameCount, DEFAULT_FRAME_DELAY);} /** * Creates a new animated icon with the specified number of frames and repaint delay. * @param frameCount number of frames in the animation. * @param repaintDelay number of milliseconds to sleep between each frame. */ public AnimatedIcon(int frameCount, int repaintDelay) { // Initialises the animation timer. timer = new Timer(repaintDelay, new AnimationUpdater(this)); timer.setRepeats(true); // Initialises frame control. setFrameCount(frameCount); setFrameDelay(repaintDelay); } // - Abstract methods ---------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Returns the icon's width. * @return the icon's width. */ public abstract int getIconWidth(); /** * Returns the icon's height. * @return the icon's height. */ public abstract int getIconHeight(); /** * Paints the current frame. * @param c component in which the frame is being painted. * @param g graphics in which to paint the frame. * @param x horizontal coordinate at which to paint the frame. * @param y vertical coordinate at which to paint the frame. */ protected abstract void paintFrame(Component c, Graphics g, int x, int y); // - Frame management ---------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Sets the total number of frames in the animation. * @param count total number of frames in the animation. */ public synchronized void setFrameCount(int count) {this.frameCount = count;} /** * Returns the total number of frames in the animation. * @return the total number of frames in the animation. */ public synchronized int getFrameCount() {return frameCount;} /** * Returns the index of the current frame in the animation. * @return the index of the current frame in the animation. */ public synchronized int getFrame() {return currentFrame;} /** * Sets the index of the current frame in the animation. * <p> * If the method does actually change the current frame, it will trigger a repaint. * </p> * @param frame index of the current frame in the animation. */ public synchronized void setFrame(int frame) { if(frame != currentFrame) { if(frame == 0) currentFrame = 0; else currentFrame = frame % frameCount; repaint(); } } /** * Takes the animation to its next frame. * <p> * This is a convenience method and is strictly equivalent to calling * <code>{@link #setFrame(int) setFrame}({@link #getFrame() getFrame()} + 1)</code>. * </p> */ public synchronized void nextFrame() {setFrame(currentFrame + 1);} /** * Sets the number of milliseconds the animation will sleep between each frame. * <p> * If set to 0, the animation will stop. * </p> * @param delay number of milliseconds the animation will sleep between each frame. */ public synchronized void setFrameDelay(int delay) {timer.setDelay(delay);} /** * Starts / stops the animation. * @param a whether the animation should be started or stopped. */ public synchronized void setAnimated(boolean a) { // Starts the animation if necessary. if(a) { if(!timer.isRunning()) timer.restart(); } // Stops the animation if necessary. else if(timer.isRunning()) timer.stop(); animate = a; } /** * Returns <code>true</code> if the animation is currently running. * <p> * Note that this method will return <code>true</code> if the animation is <b>meant</b> to be running, * for example if the icon is not visible but would be animated if it was. * </p> * @return <code>true</code> if the animation is currently running, <code>false</code>. */ public synchronized boolean isAnimated() {return animate;} /** * Returns the number of milliseconds the animation will sleep between each frame. * @return the number of milliseconds the animation will sleep between each frame. */ public synchronized int getFrameDelay() {return timer.getDelay();} // - Painting ------------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** * Paints the icon's current frame. * @param c component in which to paint the icon. * @param g graphic context in which to paint the icon. * @param x horizontal coordinate at which to paint the icon. * @param y vertical coordinate at which to paint the icon. */ public synchronized void paintIcon(Component c, Graphics g, int x, int y) { // Paints the current frame. paintFrame(c, g, x, y); // Stores the component and starts / restarts the timer if necessary. if(c != null) { AffineTransform transform; transform = ((Graphics2D)g).getTransform(); components.add(new TrackedComponent(c, x, y, (int)(getIconWidth() * transform.getScaleX()), (int)(getIconHeight() * transform.getScaleY()))); // Restarts the timer if necessary. if(!timer.isRunning() && animate) timer.restart(); } } /** * Forces the icon to repaint. */ protected synchronized void repaint() { // If the component list is empty, we can stop the timer. if(components.isEmpty()) timer.stop(); // Repaints all pending components. else { for(TrackedComponent comp : components) comp.repaint(); components.clear(); } } @Override protected void finalize() throws Throwable { // Forces the timer to stop when the animation isn't used anymore. timer.stop(); super.finalize(); } // - Container tracking -------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Used to keep track of the various components in which an animated icon is being painted. * @author twall, Nicolas Rinaudo */ private static class TrackedComponent { /** Component in which the icon must be painted. */ private Component component; /** Horizontal coordinate at which the icon should be painted. */ private int x; /** Vertical coordinate at which the icon should be painted. */ private int y; /** Width of the icon (used for clipping). */ private int width; /** Height of the icon (used for clipping). */ private int height; /** Component's hashcode. */ private int hashCode; /** * Creates a new tracked component. * @param c component in which to paint the icon. * @param x horizontal coordinate at which to paint the icon. * @param y vertical coordinate at which to paint the icon. * @param width width of the icon. * @param height height of the icon. */ public TrackedComponent(Component c, int x, int y, int width, int height) { Component ancestor; // Identifies the component that displays the icon. if((ancestor = findNonRendererAncestor(c)) != c) { Point pt = SwingUtilities.convertPoint(c, x, y, ancestor); c = ancestor; x = pt.x; y = pt.y; } // Stores all the necessary information and computes the tracked component's hashcode. component = c; this.x = x; this.y = y; this.width = width; this.height = height; hashCode = (x + "," + y + ":" + c.hashCode()).hashCode(); } public int hashCode() {return hashCode;} /** * Finds the specified component's first non-renderer ancestor. * @param c component whose ancestors should be explored. */ private Component findNonRendererAncestor(Component c) { Component ancestor; ancestor = SwingUtilities.getAncestorOfClass(CellRendererPane.class, c); if (ancestor != null && ancestor != c && ancestor.getParent() != null) c = findNonRendererAncestor(ancestor.getParent()); return c; } /** * Forces the tracked component to repaint the animated icon. */ public void repaint() {component.repaint(x, y, width, height);} } // - Timer management ---------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Receives timer events and notifies the icon. * @author twall, Nicolas Rinaudo */ private static class AnimationUpdater implements ActionListener { /** Weak reference to the animation. */ private WeakReference<AnimatedIcon> icon; /** * Creates a new animation updater on the specified icon. * @param icon animation to update. */ public AnimationUpdater(AnimatedIcon icon) {this.icon = new WeakReference<AnimatedIcon>(icon);} /** * Notifies the icon that it should update. * @param event ignored. */ public void actionPerformed(ActionEvent event) { AnimatedIcon i; // Makes sure the animation hasn't been garbage collected. if((i = icon.get()) != null) i.nextFrame(); } } }