001 /* 002 * Copyright (c) 2005, romain guy (romain.guy@jext.org) and craig wickesser (craig@codecraig.com) 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 006 * 007 * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 008 * * 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. 009 * * 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. 010 * 011 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 012 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 013 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 014 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 015 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 016 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF 017 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 018 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 019 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 020 * POSSIBILITY OF SUCH DAMAGE. 021 */ 022 023 package net.java.swingfx.waitwithstyle; 024 import java.awt.Color; 025 import java.awt.Graphics; 026 import java.awt.Graphics2D; 027 import java.awt.RenderingHints; 028 import java.awt.event.MouseEvent; 029 import java.awt.event.MouseListener; 030 import java.awt.font.FontRenderContext; 031 import java.awt.font.TextLayout; 032 import java.awt.geom.AffineTransform; 033 import java.awt.geom.Area; 034 import java.awt.geom.Ellipse2D; 035 import java.awt.geom.Point2D; 036 import java.awt.geom.Rectangle2D; 037 038 import javax.swing.JComponent; 039 040 /** 041 * An infinite progress panel displays a rotating figure and 042 * a message to notice the user of a long, duration unknown 043 * task. The shape and the text are drawn upon a white veil 044 * which alpha level (or shield value) lets the underlying 045 * component shine through. This panel is meant to be used 046 * asa <i>glass pane</i> in the window performing the long 047 * operation. 048 * <br /><br /> 049 * On the contrary to regular glass panes, you don't need to 050 * set it visible or not by yourself. Once you've started the 051 * animation all the mouse events are intercepted by this 052 * panel, preventing them from being forwared to the 053 * underlying components. 054 * <br /><br /> 055 * The panel can be controlled by the <code>start()</code>, 056 * <code>stop()</code> and <code>interrupt()</code> methods. 057 * <br /><br /> 058 * Example: 059 * <br /><br /> 060 * <pre>InfiniteProgressPanel pane = new InfiniteProgressPanel(); 061 * frame.setGlassPane(pane); 062 * pane.start()</pre> 063 * <br /><br /> 064 * Several properties can be configured at creation time. The 065 * message and its font can be changed at runtime. Changing the 066 * font can be done using <code>setFont()</code> and 067 * <code>setForeground()</code>.<br /><br /> 068 * If you experience performance issues, prefer the 069 * <code>PerformanceInfiniteProgressPanel</code>. 070 * 071 * @author Romain Guy, 17/02/2005 072 * @since 1.0 073 * <br> 074 * $Revision: 1.2 $ 075 */ 076 077 public class InfiniteProgressPanel extends JComponent implements MouseListener { 078 private static final long serialVersionUID = 3546080263571714356L; 079 080 /** Contains the bars composing the circular shape. */ 081 protected Area[] ticker = null; 082 /** The animation thread is responsible for fade in/out and rotation. */ 083 protected Thread animation = null; 084 /** Notifies whether the animation is running or not. */ 085 protected boolean started = false; 086 /** Alpha level of the veil, used for fade in/out. */ 087 protected int alphaLevel = 0; 088 /** Duration of the veil's fade in/out. */ 089 protected int rampDelay = 300; 090 /** Alpha level of the veil. */ 091 protected float shield = 0.70f; 092 /** Message displayed below the circular shape. */ 093 protected String text = ""; 094 /** Amount of bars composing the circular shape. */ 095 protected int barsCount = 14; 096 /** Amount of frames per seconde. Lowers this to save CPU. */ 097 protected float fps = 15.0f; 098 /** Rendering hints to set anti aliasing. */ 099 protected RenderingHints hints = null; 100 101 /** 102 * Creates a new progress panel with default values:<br /> 103 * <ul> 104 * <li>No message</li> 105 * <li>14 bars</li> 106 * <li>Veil's alpha level is 70%</li> 107 * <li>15 frames per second</li> 108 * <li>Fade in/out last 300 ms</li> 109 * </ul> 110 */ 111 public InfiniteProgressPanel() { 112 this(""); 113 } 114 115 /** 116 * Creates a new progress panel with default values:<br /> 117 * <ul> 118 * <li>14 bars</li> 119 * <li>Veil's alpha level is 70%</li> 120 * <li>15 frames per second</li> 121 * <li>Fade in/out last 300 ms</li> 122 * </ul> 123 * @param text The message to be displayed. Can be null or empty. 124 */ 125 public InfiniteProgressPanel(String text) { 126 this(text, 14); 127 } 128 129 /** 130 * Creates a new progress panel with default values:<br /> 131 * <ul> 132 * <li>Veil's alpha level is 70%</li> 133 * <li>15 frames per second</li> 134 * <li>Fade in/out last 300 ms</li> 135 * </ul> 136 * @param text The message to be displayed. Can be null or empty. 137 * @param barsCount The amount of bars composing the circular shape 138 */ 139 public InfiniteProgressPanel(String text, int barsCount) { 140 this(text, barsCount, 0.70f); 141 } 142 143 /** 144 * Creates a new progress panel with default values:<br /> 145 * <ul> 146 * <li>15 frames per second</li> 147 * <li>Fade in/out last 300 ms</li> 148 * </ul> 149 * @param text The message to be displayed. Can be null or empty. 150 * @param barsCount The amount of bars composing the circular shape. 151 * @param shield The alpha level between 0.0 and 1.0 of the colored 152 * shield (or veil). 153 */ 154 public InfiniteProgressPanel(String text, int barsCount, float shield) { 155 this(text, barsCount, shield, 15.0f); 156 } 157 158 /** 159 * Creates a new progress panel with default values:<br /> 160 * <ul> 161 * <li>Fade in/out last 300 ms</li> 162 * </ul> 163 * @param text The message to be displayed. Can be null or empty. 164 * @param barsCount The amount of bars composing the circular shape. 165 * @param shield The alpha level between 0.0 and 1.0 of the colored 166 * shield (or veil). 167 * @param fps The number of frames per second. Lower this value to 168 * decrease CPU usage. 169 */ 170 public InfiniteProgressPanel(String text, int barsCount, float shield, float fps) { 171 this(text, barsCount, shield, fps, 300); 172 } 173 174 /** 175 * Creates a new progress panel. 176 * @param text The message to be displayed. Can be null or empty. 177 * @param barsCount The amount of bars composing the circular shape. 178 * @param shield The alpha level between 0.0 and 1.0 of the colored 179 * shield (or veil). 180 * @param fps The number of frames per second. Lower this value to 181 * decrease CPU usage. 182 * @param rampDelay The duration, in milli seconds, of the fade in and 183 * the fade out of the veil. 184 */ 185 public InfiniteProgressPanel(String text, int barsCount, float shield, float fps, int rampDelay) { 186 this.text = text; 187 this.rampDelay = rampDelay >= 0 ? rampDelay : 0; 188 this.shield = shield >= 0.0f ? shield : 0.0f; 189 this.fps = fps > 0.0f ? fps : 15.0f; 190 this.barsCount = barsCount > 0 ? barsCount : 14; 191 192 this.hints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); 193 this.hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 194 this.hints.put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); 195 } 196 197 /** 198 * Changes the displayed message at runtime. 199 * 200 * @param text The message to be displayed. Can be null or empty. 201 */ 202 public void setText(String text) { 203 this.text = text; 204 repaint(); 205 } 206 207 /** 208 * Returns the current displayed message. 209 */ 210 public String getText() { 211 return text; 212 } 213 214 /** 215 * Starts the waiting animation by fading the veil in, then 216 * rotating the shapes. This method handles the visibility 217 * of the glass pane. 218 */ 219 public void start() { 220 addMouseListener(this); 221 setVisible(true); 222 ticker = buildTicker(); 223 animation = new Thread(new Animator(true)); 224 animation.start(); 225 } 226 227 /** 228 * Stops the waiting animation by stopping the rotation 229 * of the circular shape and then by fading out the veil. 230 * This methods sets the panel invisible at the end. 231 */ 232 public void stop() { 233 if (animation != null) { 234 animation.interrupt(); 235 try { 236 animation.join(); 237 } catch (InterruptedException ie) { } 238 animation = null; 239 240 animation = new Thread(new Animator(false)); 241 animation.start(); 242 } 243 } 244 245 /** 246 * Interrupts the animation, whatever its state is. You 247 * can use it when you need to stop the animation without 248 * running the fade out phase. 249 * This methods sets the panel invisible at the end. 250 */ 251 public void interrupt() { 252 if (animation != null) { 253 animation.interrupt(); 254 animation = null; 255 256 removeMouseListener(this); 257 setVisible(false); 258 } 259 } 260 261 public void paintComponent(Graphics g) { 262 if (started) { 263 int width = getWidth(); 264 int height = getHeight(); 265 266 double maxY = 0.0; 267 268 Graphics2D g2 = (Graphics2D) g; 269 g2.setRenderingHints(hints); 270 271 g2.setColor(new Color(255, 255, 255, (int) (alphaLevel * shield))); 272 g2.fillRect(0, 0, getWidth(), getHeight()); 273 274 for (int i = 0; i < ticker.length; i++) { 275 int channel = 224 - 128 / (i + 1); 276 g2.setColor(new Color(channel, channel, channel, alphaLevel)); 277 g2.fill(ticker[i]); 278 279 Rectangle2D bounds = ticker[i].getBounds2D(); 280 if (bounds.getMaxY() > maxY) 281 maxY = bounds.getMaxY(); 282 } 283 284 if (text != null && text.length() > 0) { 285 FontRenderContext context = g2.getFontRenderContext(); 286 TextLayout layout = new TextLayout(text, getFont(), context); 287 Rectangle2D bounds = layout.getBounds(); 288 g2.setColor(getForeground()); 289 layout.draw(g2, (float) (width - bounds.getWidth()) / 2, 290 (float) (maxY + layout.getLeading() + 2 * layout.getAscent())); 291 } 292 } 293 } 294 295 /** 296 * Builds the circular shape and returns the result as an array of 297 * <code>Area</code>. Each <code>Area</code> is one of the bars 298 * composing the shape. 299 */ 300 private Area[] buildTicker() { 301 Area[] ticker = new Area[barsCount]; 302 Point2D.Double center = new Point2D.Double((double) getWidth() / 2, (double) getHeight() / 2); 303 double fixedAngle = 2.0 * Math.PI / ((double) barsCount); 304 305 for (double i = 0.0; i < (double) barsCount; i++) { 306 Area primitive = buildPrimitive(); 307 308 AffineTransform toCenter = AffineTransform.getTranslateInstance(center.getX(), center.getY()); 309 AffineTransform toBorder = AffineTransform.getTranslateInstance(45.0, -6.0); 310 AffineTransform toCircle = AffineTransform.getRotateInstance(-i * fixedAngle, center.getX(), center.getY()); 311 312 AffineTransform toWheel = new AffineTransform(); 313 toWheel.concatenate(toCenter); 314 toWheel.concatenate(toBorder); 315 316 primitive.transform(toWheel); 317 primitive.transform(toCircle); 318 319 ticker[(int) i] = primitive; 320 } 321 322 return ticker; 323 } 324 325 /** 326 * Builds a bar. 327 */ 328 private Area buildPrimitive() 329 { 330 Rectangle2D.Double body = new Rectangle2D.Double(6, 0, 30, 12); 331 Ellipse2D.Double head = new Ellipse2D.Double(0, 0, 12, 12); 332 Ellipse2D.Double tail = new Ellipse2D.Double(30, 0, 12, 12); 333 334 Area tick = new Area(body); 335 tick.add(new Area(head)); 336 tick.add(new Area(tail)); 337 338 return tick; 339 } 340 341 /** 342 * Animation thread. 343 */ 344 private class Animator implements Runnable { 345 private boolean rampUp = true; 346 347 protected Animator(boolean rampUp) { 348 this.rampUp = rampUp; 349 } 350 351 public void run() { 352 Point2D.Double center = new Point2D.Double((double) getWidth() / 2, (double) getHeight() / 2); 353 double fixedIncrement = 2.0 * Math.PI / ((double) barsCount); 354 AffineTransform toCircle = AffineTransform.getRotateInstance(fixedIncrement, center.getX(), center.getY()); 355 356 long start = System.currentTimeMillis(); 357 if (rampDelay == 0) 358 alphaLevel = rampUp ? 255 : 0; 359 360 started = true; 361 boolean inRamp = rampUp; 362 363 while (!Thread.interrupted()) { 364 if (!inRamp) { 365 for (int i = 0; i < ticker.length; i++) 366 ticker[i].transform(toCircle); 367 } 368 369 repaint(); 370 371 if (rampUp) { 372 if (alphaLevel < 255) { 373 alphaLevel = (int) (255 * (System.currentTimeMillis() - start) / rampDelay); 374 if (alphaLevel >= 255) { 375 alphaLevel = 255; 376 inRamp = false; 377 } 378 } 379 } else if (alphaLevel > 0) { 380 alphaLevel = (int) (255 - (255 * (System.currentTimeMillis() - start) / rampDelay)); 381 if (alphaLevel <= 0) { 382 alphaLevel = 0; 383 break; 384 } 385 } 386 387 try { 388 Thread.sleep(inRamp ? 10 : (int) (1000 / fps)); 389 } catch (InterruptedException ie) { 390 break; 391 } 392 Thread.yield(); 393 } 394 395 if (!rampUp) { 396 started = false; 397 repaint(); 398 399 setVisible(false); 400 removeMouseListener(InfiniteProgressPanel.this); 401 } 402 } 403 } 404 405 public void mouseClicked(MouseEvent e) { 406 } 407 408 public void mousePressed(MouseEvent e) { 409 } 410 411 public void mouseReleased(MouseEvent e) { 412 } 413 414 public void mouseEntered(MouseEvent e) { 415 } 416 417 public void mouseExited(MouseEvent e) { 418 } 419 }