/* * 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 java.awt.*; import java.awt.image.BufferedImage; /** * Animated icon of a spinning dial used to notify users that an application is performing a task. * <p> * This behaves as any animated icon except for one thing: when the animation is stopped using * {@link #setAnimated(boolean)}, the dial won't be displayed anymore until the animation is * resumed. * </p> * <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 class SpinningDial extends AnimatedIcon { // - Class constants ----------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Default creation animation status. */ public static final boolean DEFAULT_ANIMATE = false; /** Dial's default color. */ public static final Color DEFAULT_COLOR = Color.BLACK; /** Minimum alpha-transparency value that must be applied to the dial's color as it fades out. */ private static final int MIN_ALPHA = 32; /** Icon's default width and height. */ public static final int DEFAULT_SIZE = 16; /** Default number of spokes in the dial. */ public static final int DEFAULT_SPOKES = 12; /** Dial's full size, will be scaled down at paint time. */ private static final int FULL_SIZE = 256; /** Width of each of the dial's strokes. */ private static final float DEFAULT_STROKE_WIDTH = FULL_SIZE / 10f; /** Scale down factor for the dial. */ private static final float FRACTION = 0.6f; // - Instance fields ----------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Icon's width. */ private int width; /** Icon's height. */ private int height; /** All images that compose the spinning dial. */ private Image[] frames; /** Color used to paint the dial. */ private Color color; /** Width of each stroke. */ private float strokeWidth; // - Initialisation ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** * Creates a new spinning dial. * <p> * The new instance will be initialised using default values: * <ul> * <li>{@link #DEFAULT_SIZE} for its width and height.</li> * <li>{@link #DEFAULT_COLOR} for its color.</li> * <li>{@link #DEFAULT_SPOKES} for its number of spokes.</li> * </ul> * </p> * <p> * A dial created that way will not be displayed until {@link #setAnimated(boolean)} is * called to animate it. * </p> */ public SpinningDial() {this(DEFAULT_SIZE, DEFAULT_SIZE);} /** * Creates a new spinning dial. * <p> * The new instance will be initialised using default values: * <ul> * <li>{@link #DEFAULT_SIZE} for its width and height.</li> * <li>{@link #DEFAULT_COLOR} for its color.</li> * <li>{@link #DEFAULT_SPOKES} for its number of spokes.</li> * </ul> * </p> * @param animate whether to animate the dial immediately or not. */ public SpinningDial(boolean animate) {this(DEFAULT_SIZE, DEFAULT_SIZE, animate);} /** * Creates a new spinning dial with the specified color. * <p> * The new instance will be initialised using default values: * <ul> * <li>{@link #DEFAULT_SIZE} for its width and height.</li> * <li>{@link #DEFAULT_SPOKES} for its number of spokes.</li> * </ul> * </p> * <p> * A dial created that way will not be displayed until {@link #setAnimated(boolean)} is * called to animate it. * </p> * @param c color in which to paint the dial. */ public SpinningDial(Color c) {this(DEFAULT_SIZE, DEFAULT_SIZE, c);} /** * Creates a new spinning dial with the specified color. * <p> * The new instance will be initialised using default values: * <ul> * <li>{@link #DEFAULT_SIZE} for its width and height.</li> * <li>{@link #DEFAULT_SPOKES} for its number of spokes.</li> * </ul> * </p> * @param c color in which to paint the dial. * @param animate whether to animate the dial immediately or not. */ public SpinningDial(Color c, boolean animate) {this(DEFAULT_SIZE, DEFAULT_SIZE, c, animate);} /** * Creates a new spinning dial with the specified dimensions. * <p> * The new instance will be initialised using default values: * <ul> * <li>{@link #DEFAULT_COLOR} for its color.</li> * <li>{@link #DEFAULT_SPOKES} for its number of spokes.</li> * </ul> * </p> * <p> * A dial created that way will not be displayed until {@link #setAnimated(boolean)} is * called to animate it. * </p> * @param w width of the icon. * @param h height of the icon. */ public SpinningDial(int w, int h) {this(w, h, DEFAULT_SPOKES);} /** * Creates a new spinning dial with the specified dimensions. * <p> * The new instance will be initialised using default values: * <ul> * <li>{@link #DEFAULT_COLOR} for its color.</li> * <li>{@link #DEFAULT_SPOKES} for its number of spokes.</li> * </ul> * </p> * @param w width of the icon. * @param h height of the icon. * @param animate whether to animate the dial immediately or not. */ public SpinningDial(int w, int h, boolean animate) {this(w, h, DEFAULT_SPOKES, animate);} /** * Creates a new spinning dial with the specified dimensions and color. * <p> * The new instance will use {@link #DEFAULT_SPOKES} for its number of spokes. * </p> * <p> * A dial created that way will not be displayed until {@link #setAnimated(boolean)} is * called to animate it. * </p> * @param w width of the icon. * @param h height of the icon. * @param c color in which to paint the dial. */ public SpinningDial(int w, int h, Color c) {this(w, h, DEFAULT_SPOKES, c);} /** * Creates a new spinning dial with the specified dimensions and color. * <p> * The new instance will use {@link #DEFAULT_SPOKES} for its number of spokes. * </p> * @param w width of the icon. * @param h height of the icon. * @param c color in which to paint the dial. * @param animate whether to animate the dial immediately or not. */ public SpinningDial(int w, int h, Color c, boolean animate) {this(w, h, DEFAULT_SPOKES, c, animate);} /** * Creates a new spinning dial with the specified dimensions and number of spokes. * <p> * The new instance will use {@link #DEFAULT_COLOR} for its color. * </p> * <p> * A dial created that way will not be displayed until {@link #setAnimated(boolean)} is * called to animate it. * </p> * @param w width of the icon. * @param h height of the icon. * @param spokes number of spokes that compose the dial. */ public SpinningDial(int w, int h, int spokes) {this(w, h, spokes, DEFAULT_COLOR);} /** * Creates a new spinning dial with the specified dimensions and number of spokes. * <p> * The new instance will use {@link #DEFAULT_COLOR} for its color. * </p> * @param w width of the icon. * @param h height of the icon. * @param spokes number of spokes that compose the dial. * @param animate whether to animate the dial immediately or not. */ public SpinningDial(int w, int h, int spokes, boolean animate) {this(w, h, spokes, DEFAULT_COLOR, animate);} /** * Creates a new spinning dial with the specified characteristics. * <p> * A dial created that way will not be displayed until {@link #setAnimated(boolean)} is * called to animate it. * </p> * @param w width of the icon. * @param h height of the icon. * @param spokes number of spokes that compose the dial. * @param c color in which to paint the dial. */ public SpinningDial(int w, int h, int spokes, Color c) {this(w, h, spokes, c, DEFAULT_ANIMATE);} /** * Creates a new spinning dial with the specified characteristics. * @param w width of the icon. * @param h height of the icon. * @param spokes number of spokes that compose the dial. * @param c color in which to paint the dial. * @param animate whether to animate the dial immediately or not. */ public SpinningDial(int w, int h, int spokes, Color c, boolean animate) { super(spokes, 1000 / spokes); // Initialises the icon. width = w; height = h; color = c; frames = new Image[getFrameCount()]; strokeWidth = DEFAULT_STROKE_WIDTH; // Animates the icon if necessary. if(animate) setAnimated(true); } /** * Sets the width of the strokes used to paint each of the dial's spokes. * @param width width of the strokes used to paint each of the dial's spokes. */ public synchronized void setStrokeWidth(float width) {strokeWidth = width;} /** * Returns the width of the strokes used to paint each of the dial's spokes. * @return the width of the strokes used to paint each of the dial's spokes. */ public synchronized float getStrokeWidth() {return strokeWidth;} // - Color management ---------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Sets the color used to draw the dial. * @param c color in which to paint the dial. */ public synchronized void setColor(Color c) { // Ignores calls that don't actually change anything. if(!color.equals(c)) { color = c; // Resets stored images to make sure they get repainted // with the right color. for(int i = 0; i < frames.length; i++) frames[i] = null; } } /** * Returns the color used to paint the dial. * @return the color used to paint the dial. */ public synchronized Color getColor() {return color;} /** * Computes the dial color according to the specified alpha-transparency value. * @param alpha transparency value that must be applied to the dial's color. */ protected Color getSpokeColor(int alpha) {return new Color(color.getRed(), color.getGreen(), color.getBlue(), Math.max(MIN_ALPHA, alpha));} // - Size methods -------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Returns the icon's height. * @return the icon's height. */ @Override public int getIconHeight() {return height;} /** * Returns the icon's width. * @return the icon's width. */ @Override public int getIconWidth() {return width;} // - Rendering methods --------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Initialises graphics for painting one of the dial's frames. * @param graphics graphics instance to initialise. */ private void initialiseGraphics(Graphics2D graphics) { float scale; scale = (float)Math.min(width, height) / FULL_SIZE; graphics.setComposite(AlphaComposite.Clear); graphics.fillRect(0, 0, width, height); graphics.setComposite(AlphaComposite.Src); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); graphics.translate((float)width / 2, (float)height / 2); graphics.scale(scale, scale); } /** * Paints the current frame on the specified component. * @param c component on which to paint the dial. * @param graphics graphic context to use when painting the dial. * @param x horizontal coordinate at which to paint the dial. * @param y vertical coordinate at which to paint the dial. */ @Override public synchronized void paintFrame(Component c, Graphics graphics, int x, int y) { int currentFrame; // Ignores paint calls while not animated. if(isAnimated()) { // Checks whether the current frame has already been generated or not, generates // it if not. if((frames[currentFrame = getFrame()]) == null) { Image frame; GraphicsConfiguration gc; Graphics2D g; int alpha; double cos; double sin; int radius; // Initialises the frame. // Note: getGraphicsConfiguration() returns null if the component has not yet been added to a container if(c!=null && (gc=c.getGraphicsConfiguration())!=null) frame = gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT); else frame = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // Initialises the frame's g. initialiseGraphics(g = (Graphics2D)frame.getGraphics()); // Draws each spoke in the dial. alpha = 255; radius = FULL_SIZE / 2 - 1 - (int)(strokeWidth / 2); for(int i = 0; i < getFrameCount(); i++) { cos = Math.cos((Math.PI * 2) - (Math.PI * 2 * (i - currentFrame)) / getFrameCount()); sin = Math.sin((Math.PI * 2) - (Math.PI * 2 * (i - currentFrame)) / getFrameCount()); g.setColor(getSpokeColor(alpha)); g.drawLine((int)(radius * FRACTION * cos), (int)(radius * FRACTION * sin), (int)(radius * cos), (int)(radius * sin)); alpha = Math.max(MIN_ALPHA, (alpha * 3) / 4); } g.dispose(); // Stores the newly generated frame. frames[currentFrame] = frame; } // Draws the current frame. graphics.drawImage(frames[currentFrame], x, y, null); } } /** * Starts / stops the spinning dial. * <p> * If <code>a</code> is <code>false</code>, the animation will stop and the * the dial won't be displayed anymore until the animationr resumes. * </p> * @param a whether to start or stop the animation. */ @Override public void setAnimated(boolean a) { super.setAnimated(a); // Makes sure the dial disapears when the animation is stopped. if(!a) repaint(); } }