/* * Copyright (c) 2005, romain guy (romain.guy@jext.org) and craig wickesser (craig@codecraig.com) * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * * Neither the name of the <ORGANIZATION> nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package net.java.swingfx.waitwithstyle; import java.awt.Color; import java.awt.Cursor; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.font.FontRenderContext; import java.awt.font.TextLayout; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.geom.Ellipse2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import javax.swing.JComponent; /** * An infinite progress panel displays a rotating figure and a message to notice * the user of a long, duration unknown task. The shape and the text are drawn * upon a white veil which alpha level (or shield value) lets the underlying * component shine through. This panel is meant to be used asa <i>glass pane</i> * in the window performing the long operation. * <p> * Contrary to regular glass panes, you don't need to set it visible or not by * yourself. Once you've started the animation all the mouse events are * intercepted by this panel, preventing them from being forwared to the * underlying components. * <p> * The panel can be controlled by the <code>start()</code>, <code>stop()</code> * and <code>interrupt()</code> methods. * <p> * Example: * <p> * * <pre> * InfiniteProgressPanel pane = new InfiniteProgressPanel(); * frame.setGlassPane(pane); * ... later in some other EDT event (otherwise the panel doesn't know the size and draws real funky) * pane.start() * </pre> * <p> * Several properties can be configured at creation time. The message and its * font can be changed at runtime. Changing the font can be done using * <code>setFont()</code> and <code>setForeground()</code>. * <p> * If you experience performance issues, prefer the * <code>PerformanceInfiniteProgressPanel</code>. * <p> * For cancelable progress use the <code>CancelableProgressPanel</code> or the * <code>CancelableProgressAdapter</code> with a Panel. * * @author Romain Guy, 17/02/2005 * @since 1.0 <br> * $Revision: 1.5 $ */ public class InfiniteProgressPanel extends JComponent implements MouseListener, CancelableAdaptee { private static final long serialVersionUID = 3546080263571714356L; /** Contains the bars composing the circular shape. */ protected Ticker ticker = null; /** * The animation thread is responsible for fade in/out and rotation. */ protected Thread animation = null; /** * Notifies whether the animation is running or not. */ protected boolean started = false; /** * Alpha level of the veil, used for fade in/out. */ protected int alphaLevel = 0; /** * Duration of the veil's fade in/out. */ protected int rampDelay = 300; /** * Alpha level of the veil. */ protected float shield = 0.70f; /** * Message displayed below the circular shape. */ protected String text = ""; /** * Amount of bars composing the circular shape. */ protected int barsCount = 14; /** * Amount of frames per seconde. Lowers this to save CPU. */ protected float fps = 15.0f; /** * Rendering hints to set anti aliasing. */ protected RenderingHints hints = null; /** * An infiniteProgressAdapter to performa special drawing, ex: a cancel * button. */ protected InfiniteProgressAdapter infiniteProgressAdapter = null; /** * Creates a new progress panel with default values:<br /> * <ul> * <li>No message</li> * <li>14 bars</li> * <li>Veil's alpha level is 70%</li> * <li>15 frames per second</li> * <li>Fade in/out last 300 ms</li> * </ul> */ public InfiniteProgressPanel() { this(""); } /** * Creates a new progress panel with default values:<br /> * <ul> * <li>14 bars</li> * <li>Veil's alpha level is 70%</li> * <li>15 frames per second</li> * <li>Fade in/out last 300 ms</li> * </ul> * * @param text * The message to be displayed. Can be null or empty. */ public InfiniteProgressPanel(final String text) { this(text, 14); } /** * Creates a new progress panel with default values:<br /> * <ul> * <li>Veil's alpha level is 70%</li> * <li>15 frames per second</li> * <li>Fade in/out last 300 ms</li> * </ul> * * @param text * The message to be displayed. Can be null or empty. * @param barsCount * The amount of bars composing the circular shape */ public InfiniteProgressPanel(final String text, final int barsCount) { this(text, barsCount, 0.70f); } /** * Creates a new progress panel with default values:<br /> * <ul> * <li>15 frames per second</li> * <li>Fade in/out last 300 ms</li> * </ul> * * @param text * The message to be displayed. Can be null or empty. * @param barsCount * The amount of bars composing the circular shape. * @param shield * The alpha level between 0.0 and 1.0 of the colored shield (or * veil). */ public InfiniteProgressPanel(final String text, final int barsCount, final float shield) { this(text, barsCount, shield, 15.0f); } /** * Creates a new progress panel with default values:<br /> * <ul> * <li>Fade in/out last 300 ms</li> * </ul> * * @param text * The message to be displayed. Can be null or empty. * @param barsCount * The amount of bars composing the circular shape. * @param shield * The alpha level between 0.0 and 1.0 of the colored shield (or * veil). * @param fps * The number of frames per second. Lower this value to decrease * CPU usage. */ public InfiniteProgressPanel(final String text, final int barsCount, final float shield, final float fps) { this(text, barsCount, shield, fps, 300); } /** * Creates a new progress panel. * * @param text * The message to be displayed. Can be null or empty. * @param barsCount * The amount of bars composing the circular shape. * @param shield * The alpha level between 0.0 and 1.0 of the colored shield (or * veil). * @param fps * The number of frames per second. Lower this value to decrease * CPU usage. * @param rampDelay * The duration, in milli seconds, of the fade in and the fade * out of the veil. */ public InfiniteProgressPanel(final String text, final int barsCount, final float shield, final float fps, final int rampDelay) { this.text = text; this.rampDelay = rampDelay >= 0 ? rampDelay : 0; this.shield = shield >= 0.0f ? shield : 0.0f; this.fps = fps > 0.0f ? fps : 15.0f; this.barsCount = barsCount > 0 ? barsCount : 14; this.hints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); this.hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); this.hints.put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); } /** * Changes the displayed message at runtime. * * @param text * The message to be displayed. Can be null or empty. */ @Override public void setText(final String text) { this.text = text; this.repaint(); } /** * Returns the current displayed message. */ public String getText() { return this.text; } /** * @param infiniteProgressAdapter * an infiniteProgressAdapter to perform special drawing (ex: a * cancel button) */ public void setInfiniteProgressAdapter( final InfiniteProgressAdapter infiniteProgressAdapter) { this.infiniteProgressAdapter = infiniteProgressAdapter; } /** * Adds a listener to the cancel button in this progress panel. * * @throws RuntimeException * if the infiniteProgressAdapter is null or is not a * CancelableProgessAdapter * @param listener */ @Override public void addCancelListener(final ActionListener listener) { if (this.infiniteProgressAdapter instanceof CancelableProgessAdapter) { ((CancelableProgessAdapter) this.infiniteProgressAdapter) .addCancelListener(listener); } else { throw new RuntimeException( "Expected CancelableProgessAdapter for cancel listener. Adapter is " + this.infiniteProgressAdapter); } } /** * Removes a listener to the cancel button in this progress panel. * * @throws RuntimeException * if the infiniteProgressAdapter is null or is not a * CancelableProgessAdapter * @param listener */ @Override public void removeCancelListener(final ActionListener listener) { if (this.infiniteProgressAdapter instanceof CancelableProgessAdapter) { ((CancelableProgessAdapter) this.infiniteProgressAdapter) .removeCancelListener(listener); } else { throw new RuntimeException( "Expected CancelableProgessAdapter for cancel listener. Adapter is " + this.infiniteProgressAdapter); } } /** * Starts the waiting animation by fading the veil in, then rotating the * shapes. This method handles the visibility of the glass pane. */ @Override public void start() { this.addMouseListener(this); this.setVisible(true); this.buildTicker(); this.animation = new Thread(new Animator(true)); if (this.infiniteProgressAdapter != null) { this.infiniteProgressAdapter.animationStarting(); } this.animation.start(); } /** * Stops the waiting animation by stopping the rotation of the circular * shape and then by fading out the veil. This methods sets the panel * invisible at the end. */ @Override public void stop() { if (this.infiniteProgressAdapter != null) { this.infiniteProgressAdapter.animationStopping(); } if (this.animation != null) { this.animation.interrupt(); try { this.animation.join(); } catch (final InterruptedException ie) { } this.animation = null; this.animation = new Thread(new Animator(false)); this.animation.start(); } } @Override public JComponent getComponent() { return this; } /** * Interrupts the animation, whatever its state is. You can use it when you * need to stop the animation without running the fade out phase. This * methods sets the panel invisible at the end. */ public void interrupt() { if (this.animation != null) { this.animation.interrupt(); this.animation = null; this.removeMouseListener(this); this.setVisible(false); } } @Override public void paintComponent(final Graphics g) { if (this.started) { final int width = this.getWidth(); final int height = this.getHeight(); if ((width == 0) || (height == 0)) { return; } final Ticker ticker = this.getTicker(); if (ticker == null) { return; } final Graphics2D g2 = (Graphics2D) g; g2.setRenderingHints(this.hints); g2.setColor(new Color(255, 255, 255, (int) (this.alphaLevel * this.shield))); g2.fillRect(0, 0, this.getWidth(), this.getHeight()); for (int i = 0; i < ticker.bars.length; i++) { final int channel = 224 - (128 / (i + 1)); g2.setColor(new Color(channel, channel, channel, this.alphaLevel)); g2.fill(ticker.bars[i]); } final double textMaxY = InfiniteProgressPanel.drawTextAt(this.text, this.getFont(), g2, width, ticker.maxY, this.getForeground()); if (this.infiniteProgressAdapter != null) { this.infiniteProgressAdapter.paintSubComponents(textMaxY); } } } /** * Draw text in a Graphics2D. * * @param text * the text to draw * @param font * the font to use * @param g2 * the graphics context to draw in * @param width * the width of the parent, so it can be centered * @param y * the height at which to draw * @param foreGround * the foreground color to draw in * @return the y value that is the y param + the text height. */ public static double drawTextAt(final String text, final Font font, final Graphics2D g2, final int width, double y, final Color foreGround) { if ((text != null) && (text.length() > 0)) { final FontRenderContext context = g2.getFontRenderContext(); final TextLayout layout = new TextLayout(text, font, context); final Rectangle2D bounds = layout.getBounds(); g2.setColor(foreGround); final float textX = (float) (width - bounds.getWidth()) / 2; y = (float) (y + layout.getLeading() + (2 * layout.getAscent())); layout.draw(g2, textX, (float) y); } return y; } /** * Ticker is not built until set bounds is called (or our width and height * are > 0). * * @return null if not ready, or the built ticker */ Ticker getTicker() { if (this.ticker == null) { this.buildTicker(); } return this.ticker; } /** * Builds the circular shape and returns the result as an array of * <code>Area</code>. Each <code>Area</code> is one of the bars composing * the shape. */ private void buildTicker() { final Area[] areas = new Area[this.barsCount]; final int width = this.getWidth(); final int height = this.getHeight(); // Sometimes the bounds are set, rebuild the ticker later. if ((width == 0) || (height == 0)) { return; } final Point2D.Double center = new Point2D.Double((double) width / 2, (double) height / 2); final double fixedAngle = (2.0 * Math.PI) / this.barsCount; double maxY = 0.0d; for (double i = 0.0; i < this.barsCount; i++) { final Area primitive = InfiniteProgressPanel.buildPrimitive(); final AffineTransform toCenter = AffineTransform .getTranslateInstance(center.getX(), center.getY()); final AffineTransform toBorder = AffineTransform .getTranslateInstance(45.0, -6.0); final AffineTransform toCircle = AffineTransform.getRotateInstance( -i * fixedAngle, center.getX(), center.getY()); final AffineTransform toWheel = new AffineTransform(); toWheel.concatenate(toCenter); toWheel.concatenate(toBorder); primitive.transform(toWheel); primitive.transform(toCircle); areas[(int) i] = primitive; final Rectangle2D bounds = primitive.getBounds2D(); if (bounds.getMaxY() > maxY) { maxY = bounds.getMaxY(); } } this.ticker = new Ticker(); this.ticker.bars = areas; this.ticker.maxY = maxY; } /** * Builds a bar. */ private static Area buildPrimitive() { final Rectangle2D.Double body = new Rectangle2D.Double(6, 0, 30, 12); final Ellipse2D.Double head = new Ellipse2D.Double(0, 0, 12, 12); final Ellipse2D.Double tail = new Ellipse2D.Double(30, 0, 12, 12); final Area tick = new Area(body); tick.add(new Area(head)); tick.add(new Area(tail)); return tick; } private class Ticker { public Ticker() { // TODO Auto-generated constructor stub } double maxY = 0.0; Area[] bars; } /** * Animation thread. */ private class Animator implements Runnable { private boolean rampUp = true; protected Animator(final boolean rampUp) { this.rampUp = rampUp; } @Override public void run() { final Ticker ticker = InfiniteProgressPanel.this.getTicker(); if (ticker == null) { return; } final Point2D.Double center = new Point2D.Double( (double) InfiniteProgressPanel.this.getWidth() / 2, (double) InfiniteProgressPanel.this.getHeight() / 2); final double fixedIncrement = (2.0 * Math.PI) / InfiniteProgressPanel.this.barsCount; final AffineTransform toCircle = AffineTransform.getRotateInstance( fixedIncrement, center.getX(), center.getY()); final long start = System.currentTimeMillis(); if (InfiniteProgressPanel.this.rampDelay == 0) { InfiniteProgressPanel.this.alphaLevel = this.rampUp ? 255 : 0; } InfiniteProgressPanel.this.started = true; boolean inRamp = this.rampUp; while (!Thread.interrupted()) { if (!inRamp) { for (final Area bar : ticker.bars) { bar.transform(toCircle); } } InfiniteProgressPanel.this.repaint(); if (this.rampUp) { if (InfiniteProgressPanel.this.alphaLevel < 255) { InfiniteProgressPanel.this.alphaLevel = (int) ((255 * (System .currentTimeMillis() - start)) / InfiniteProgressPanel.this.rampDelay); if (InfiniteProgressPanel.this.alphaLevel >= 255) { InfiniteProgressPanel.this.alphaLevel = 255; inRamp = false; if (InfiniteProgressPanel.this.infiniteProgressAdapter != null) { InfiniteProgressPanel.this.infiniteProgressAdapter .rampUpEnded(); } } } } else if (InfiniteProgressPanel.this.alphaLevel > 0) { InfiniteProgressPanel.this.alphaLevel = (int) (255 - ((255 * (System .currentTimeMillis() - start)) / InfiniteProgressPanel.this.rampDelay)); if (InfiniteProgressPanel.this.alphaLevel <= 0) { InfiniteProgressPanel.this.alphaLevel = 0; break; } } else { break; } try { Thread.sleep(inRamp ? 10 : (int) (1000 / InfiniteProgressPanel.this.fps)); } catch (final InterruptedException ie) { break; } Thread.yield(); } if (!this.rampUp) { InfiniteProgressPanel.this.started = false; InfiniteProgressPanel.this.repaint(); InfiniteProgressPanel.this.setVisible(false); InfiniteProgressPanel.this .removeMouseListener(InfiniteProgressPanel.this); } } } @Override public void mouseClicked(final MouseEvent e) { } @Override public void mousePressed(final MouseEvent e) { } @Override public void mouseReleased(final MouseEvent e) { } @Override public void mouseEntered(final MouseEvent e) { } @Override public void mouseExited(final MouseEvent e) { } }