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    }