1/*
2 * Copyright (c) 1997, 2014, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package javax.swing.plaf.basic;
27
28import sun.swing.SwingUtilities2;
29import java.awt.*;
30import java.awt.geom.AffineTransform;
31import java.awt.event.*;
32import javax.swing.*;
33import javax.swing.event.*;
34import javax.swing.plaf.*;
35import java.beans.PropertyChangeListener;
36import java.beans.PropertyChangeEvent;
37import java.io.Serializable;
38import sun.swing.DefaultLookup;
39
40/**
41 * A Basic L&F implementation of ProgressBarUI.
42 *
43 * @author Michael C. Albers
44 * @author Kathy Walrath
45 */
46public class BasicProgressBarUI extends ProgressBarUI {
47    private int cachedPercent;
48    private int cellLength, cellSpacing;
49    // The "selectionForeground" is the color of the text when it is painted
50    // over a filled area of the progress bar. The "selectionBackground"
51    // is for the text over the unfilled progress bar area.
52    private Color selectionForeground, selectionBackground;
53
54    private Animator animator;
55
56    /**
57     * The instance of {@code JProgressBar}.
58     */
59    protected JProgressBar progressBar;
60    /**
61     * The instance of {@code ChangeListener}.
62     */
63    protected ChangeListener changeListener;
64    private Handler handler;
65
66    /**
67     * The current state of the indeterminate animation's cycle.
68     * 0, the initial value, means paint the first frame.
69     * When the progress bar is indeterminate and showing,
70     * the default animation thread updates this variable
71     * by invoking incrementAnimationIndex()
72     * every repaintInterval milliseconds.
73     */
74    private int animationIndex = 0;
75
76    /**
77     * The number of frames per cycle. Under the default implementation,
78     * this depends on the cycleTime and repaintInterval.  It
79     * must be an even number for the default painting algorithm.  This
80     * value is set in the initIndeterminateValues method.
81     */
82    private int numFrames;   //0 1|numFrames-1 ... numFrames/2
83
84    /**
85     * Interval (in ms) between repaints of the indeterminate progress bar.
86     * The value of this method is set
87     * (every time the progress bar changes to indeterminate mode)
88     * using the
89     * "ProgressBar.repaintInterval" key in the defaults table.
90     */
91    private int repaintInterval;
92
93    /**
94     * The number of milliseconds until the animation cycle repeats.
95     * The value of this method is set
96     * (every time the progress bar changes to indeterminate mode)
97     * using the
98     * "ProgressBar.cycleTime" key in the defaults table.
99     */
100    private int cycleTime;  //must be repaintInterval*2*aPositiveInteger
101
102    //performance stuff
103    private static boolean ADJUSTTIMER = true; //makes a BIG difference;
104                                               //make this false for
105                                               //performance tests
106
107    /**
108     * Used to hold the location and size of the bouncing box (returned
109     * by getBox) to be painted.
110     *
111     * @since 1.5
112     */
113    protected Rectangle boxRect;
114
115    /**
116     * The rectangle to be updated the next time the
117     * animation thread calls repaint.  For bouncing-box
118     * animation this rect should include the union of
119     * the currently displayed box (which needs to be erased)
120     * and the box to be displayed next.
121     * This rectangle's values are set in
122     * the setAnimationIndex method.
123     */
124    private Rectangle nextPaintRect;
125
126    //cache
127    /** The component's painting area, not including the border. */
128    private Rectangle componentInnards;    //the current painting area
129    private Rectangle oldComponentInnards; //used to see if the size changed
130
131    /** For bouncing-box animation, the change in position per frame. */
132    private double delta = 0.0;
133
134    private int maxPosition = 0; //maximum X (horiz) or Y box location
135
136    /**
137     * Returns a new instance of {@code BasicProgressBarUI}.
138     *
139     * @param x a component
140     * @return a new instance of {@code BasicProgressBarUI}
141     */
142    public static ComponentUI createUI(JComponent x) {
143        return new BasicProgressBarUI();
144    }
145
146    public void installUI(JComponent c) {
147        progressBar = (JProgressBar)c;
148        installDefaults();
149        installListeners();
150        if (progressBar.isIndeterminate()) {
151            initIndeterminateValues();
152        }
153    }
154
155    public void uninstallUI(JComponent c) {
156        if (progressBar.isIndeterminate()) {
157            cleanUpIndeterminateValues();
158        }
159        uninstallDefaults();
160        uninstallListeners();
161        progressBar = null;
162    }
163
164    /**
165     * Installs default properties.
166     */
167    protected void installDefaults() {
168        LookAndFeel.installProperty(progressBar, "opaque", Boolean.TRUE);
169        LookAndFeel.installBorder(progressBar,"ProgressBar.border");
170        LookAndFeel.installColorsAndFont(progressBar,
171                                         "ProgressBar.background",
172                                         "ProgressBar.foreground",
173                                         "ProgressBar.font");
174        cellLength = UIManager.getInt("ProgressBar.cellLength");
175        if (cellLength == 0) cellLength = 1;
176        cellSpacing = UIManager.getInt("ProgressBar.cellSpacing");
177        selectionForeground = UIManager.getColor("ProgressBar.selectionForeground");
178        selectionBackground = UIManager.getColor("ProgressBar.selectionBackground");
179    }
180
181    /**
182     * Unintalls default properties.
183     */
184    protected void uninstallDefaults() {
185        LookAndFeel.uninstallBorder(progressBar);
186    }
187
188    /**
189     * Registers listeners.
190     */
191    protected void installListeners() {
192        //Listen for changes in the progress bar's data.
193        changeListener = getHandler();
194        progressBar.addChangeListener(changeListener);
195
196        //Listen for changes between determinate and indeterminate state.
197        progressBar.addPropertyChangeListener(getHandler());
198    }
199
200    private Handler getHandler() {
201        if (handler == null) {
202            handler = new Handler();
203        }
204        return handler;
205    }
206
207    /**
208     * Starts the animation thread, creating and initializing
209     * it if necessary. This method is invoked when an
210     * indeterminate progress bar should start animating.
211     * Reasons for this may include:
212     * <ul>
213     *    <li>The progress bar is determinate and becomes displayable
214     *    <li>The progress bar is displayable and becomes determinate
215     *    <li>The progress bar is displayable and determinate and this
216     *        UI is installed
217     * </ul>
218     * If you implement your own animation thread,
219     * you must override this method.
220     *
221     * @since 1.4
222     * @see #stopAnimationTimer
223     */
224    protected void startAnimationTimer() {
225        if (animator == null) {
226            animator = new Animator();
227        }
228
229        animator.start(getRepaintInterval());
230    }
231
232    /**
233     * Stops the animation thread.
234     * This method is invoked when the indeterminate
235     * animation should be stopped. Reasons for this may include:
236     * <ul>
237     *    <li>The progress bar changes to determinate
238     *    <li>The progress bar is no longer part of a displayable hierarchy
239     *    <li>This UI in uninstalled
240     * </ul>
241     * If you implement your own animation thread,
242     * you must override this method.
243     *
244     * @since 1.4
245     * @see #startAnimationTimer
246     */
247    protected void stopAnimationTimer() {
248        if (animator != null) {
249            animator.stop();
250        }
251    }
252
253    /**
254     * Removes all listeners installed by this object.
255     */
256    protected void uninstallListeners() {
257        progressBar.removeChangeListener(changeListener);
258        progressBar.removePropertyChangeListener(getHandler());
259        handler = null;
260    }
261
262
263    /**
264     * Returns the baseline.
265     *
266     * @throws NullPointerException {@inheritDoc}
267     * @throws IllegalArgumentException {@inheritDoc}
268     * @see javax.swing.JComponent#getBaseline(int, int)
269     * @since 1.6
270     */
271    public int getBaseline(JComponent c, int width, int height) {
272        super.getBaseline(c, width, height);
273        if (progressBar.isStringPainted() &&
274                progressBar.getOrientation() == JProgressBar.HORIZONTAL) {
275            FontMetrics metrics = progressBar.
276                    getFontMetrics(progressBar.getFont());
277            Insets insets = progressBar.getInsets();
278            int y = insets.top;
279            height = height - insets.top - insets.bottom;
280            return y + (height + metrics.getAscent() -
281                        metrics.getLeading() -
282                        metrics.getDescent()) / 2;
283        }
284        return -1;
285    }
286
287    /**
288     * Returns an enum indicating how the baseline of the component
289     * changes as the size changes.
290     *
291     * @throws NullPointerException {@inheritDoc}
292     * @see javax.swing.JComponent#getBaseline(int, int)
293     * @since 1.6
294     */
295    public Component.BaselineResizeBehavior getBaselineResizeBehavior(
296            JComponent c) {
297        super.getBaselineResizeBehavior(c);
298        if (progressBar.isStringPainted() &&
299                progressBar.getOrientation() == JProgressBar.HORIZONTAL) {
300            return Component.BaselineResizeBehavior.CENTER_OFFSET;
301        }
302        return Component.BaselineResizeBehavior.OTHER;
303    }
304
305    // Many of the Basic*UI components have the following methods.
306    // This component does not have these methods because *ProgressBarUI
307    //  is not a compound component and does not accept input.
308    //
309    // protected void installComponents()
310    // protected void uninstallComponents()
311    // protected void installKeyboardActions()
312    // protected void uninstallKeyboardActions()
313
314    /**
315     * Returns preferred size of the horizontal {@code JProgressBar}.
316     *
317     * @return preferred size of the horizontal {@code JProgressBar}
318     */
319    protected Dimension getPreferredInnerHorizontal() {
320        Dimension horizDim = (Dimension)DefaultLookup.get(progressBar, this,
321            "ProgressBar.horizontalSize");
322        if (horizDim == null) {
323            horizDim = new Dimension(146, 12);
324        }
325        return horizDim;
326    }
327
328    /**
329     * Returns preferred size of the vertical {@code JProgressBar}.
330     *
331     * @return preferred size of the vertical {@code JProgressBar}
332     */
333    protected Dimension getPreferredInnerVertical() {
334        Dimension vertDim = (Dimension)DefaultLookup.get(progressBar, this,
335            "ProgressBar.verticalSize");
336        if (vertDim == null) {
337            vertDim = new Dimension(12, 146);
338        }
339        return vertDim;
340    }
341
342    /**
343     * The "selectionForeground" is the color of the text when it is painted
344     * over a filled area of the progress bar.
345     *
346     * @return the color of the selected foreground
347     */
348    protected Color getSelectionForeground() {
349        return selectionForeground;
350    }
351
352    /**
353     * The "selectionBackground" is the color of the text when it is painted
354     * over an unfilled area of the progress bar.
355     *
356     * @return the color of the selected background
357     */
358    protected Color getSelectionBackground() {
359        return selectionBackground;
360    }
361
362    private int getCachedPercent() {
363        return cachedPercent;
364    }
365
366    private void setCachedPercent(int cachedPercent) {
367        this.cachedPercent = cachedPercent;
368    }
369
370    /**
371     * Returns the width (if HORIZONTAL) or height (if VERTICAL)
372     * of each of the individual cells/units to be rendered in the
373     * progress bar. However, for text rendering simplification and
374     * aesthetic considerations, this function will return 1 when
375     * the progress string is being rendered.
376     *
377     * @return the value representing the spacing between cells
378     * @see    #setCellLength
379     * @see    JProgressBar#isStringPainted
380     */
381    protected int getCellLength() {
382        if (progressBar.isStringPainted()) {
383            return 1;
384        } else {
385            return cellLength;
386        }
387    }
388
389    /**
390     * Sets the cell length.
391     *
392     * @param cellLen a new cell length
393     */
394    protected void setCellLength(int cellLen) {
395        this.cellLength = cellLen;
396    }
397
398    /**
399     * Returns the spacing between each of the cells/units in the
400     * progress bar. However, for text rendering simplification and
401     * aesthetic considerations, this function will return 0 when
402     * the progress string is being rendered.
403     *
404     * @return the value representing the spacing between cells
405     * @see    #setCellSpacing
406     * @see    JProgressBar#isStringPainted
407     */
408    protected int getCellSpacing() {
409        if (progressBar.isStringPainted()) {
410            return 0;
411        } else {
412            return cellSpacing;
413        }
414    }
415
416    /**
417     * Sets the cell spacing.
418     *
419     * @param cellSpace a new cell spacing
420     */
421    protected void setCellSpacing(int cellSpace) {
422        this.cellSpacing = cellSpace;
423    }
424
425    /**
426     * This determines the amount of the progress bar that should be filled
427     * based on the percent done gathered from the model. This is a common
428     * operation so it was abstracted out. It assumes that your progress bar
429     * is linear. That is, if you are making a circular progress indicator,
430     * you will want to override this method.
431     *
432     * @param b insets
433     * @param width a width
434     * @param height a height
435     * @return the amount of the progress bar that should be filled
436     */
437    protected int getAmountFull(Insets b, int width, int height) {
438        int amountFull = 0;
439        BoundedRangeModel model = progressBar.getModel();
440
441        if ( (model.getMaximum() - model.getMinimum()) != 0) {
442            if (progressBar.getOrientation() == JProgressBar.HORIZONTAL) {
443                amountFull = (int)Math.round(width *
444                                             progressBar.getPercentComplete());
445            } else {
446                amountFull = (int)Math.round(height *
447                                             progressBar.getPercentComplete());
448            }
449        }
450        return amountFull;
451    }
452
453    /**
454     * Delegates painting to one of two methods:
455     * paintDeterminate or paintIndeterminate.
456     */
457    public void paint(Graphics g, JComponent c) {
458        if (progressBar.isIndeterminate()) {
459            paintIndeterminate(g, c);
460        } else {
461            paintDeterminate(g, c);
462        }
463    }
464
465    /**
466     * Stores the position and size of
467     * the bouncing box that would be painted for the current animation index
468     * in <code>r</code> and returns <code>r</code>.
469     * Subclasses that add to the painting performed
470     * in this class's implementation of <code>paintIndeterminate</code> --
471     * to draw an outline around the bouncing box, for example --
472     * can use this method to get the location of the bouncing
473     * box that was just painted.
474     * By overriding this method,
475     * you have complete control over the size and position
476     * of the bouncing box,
477     * without having to reimplement <code>paintIndeterminate</code>.
478     *
479     * @param r  the Rectangle instance to be modified;
480     *           may be <code>null</code>
481     * @return   <code>null</code> if no box should be drawn;
482     *           otherwise, returns the passed-in rectangle
483     *           (if non-null)
484     *           or a new rectangle
485     *
486     * @see #setAnimationIndex
487     * @since 1.4
488     */
489    protected Rectangle getBox(Rectangle r) {
490        int currentFrame = getAnimationIndex();
491        int middleFrame = numFrames/2;
492
493        if (sizeChanged() || delta == 0.0 || maxPosition == 0.0) {
494            updateSizes();
495        }
496
497        r = getGenericBox(r);
498
499        if (r == null) {
500            return null;
501        }
502        if (middleFrame <= 0) {
503            return null;
504        }
505
506        //assert currentFrame >= 0 && currentFrame < numFrames
507        if (progressBar.getOrientation() == JProgressBar.HORIZONTAL) {
508            if (currentFrame < middleFrame) {
509                r.x = componentInnards.x
510                      + (int)Math.round(delta * (double)currentFrame);
511            } else {
512                r.x = maxPosition
513                      - (int)Math.round(delta *
514                                        (currentFrame - middleFrame));
515            }
516        } else { //VERTICAL indeterminate progress bar
517            if (currentFrame < middleFrame) {
518                r.y = componentInnards.y
519                      + (int)Math.round(delta * currentFrame);
520            } else {
521                r.y = maxPosition
522                      - (int)Math.round(delta *
523                                        (currentFrame - middleFrame));
524            }
525        }
526        return r;
527    }
528
529    /**
530     * Updates delta, max position.
531     * Assumes componentInnards is correct (e.g. call after sizeChanged()).
532     */
533    private void updateSizes() {
534        int length = 0;
535
536        if (progressBar.getOrientation() == JProgressBar.HORIZONTAL) {
537            length = getBoxLength(componentInnards.width,
538                                  componentInnards.height);
539            maxPosition = componentInnards.x + componentInnards.width
540                          - length;
541
542        } else { //VERTICAL progress bar
543            length = getBoxLength(componentInnards.height,
544                                  componentInnards.width);
545            maxPosition = componentInnards.y + componentInnards.height
546                          - length;
547        }
548
549        //If we're doing bouncing-box animation, update delta.
550        delta = 2.0 * (double)maxPosition/(double)numFrames;
551    }
552
553    /**
554     * Assumes that the component innards, max position, etc. are up-to-date.
555     */
556    private Rectangle getGenericBox(Rectangle r) {
557        if (r == null) {
558            r = new Rectangle();
559        }
560
561        if (progressBar.getOrientation() == JProgressBar.HORIZONTAL) {
562            r.width = getBoxLength(componentInnards.width,
563                                   componentInnards.height);
564            if (r.width < 0) {
565                r = null;
566            } else {
567                r.height = componentInnards.height;
568                r.y = componentInnards.y;
569            }
570          // end of HORIZONTAL
571
572        } else { //VERTICAL progress bar
573            r.height = getBoxLength(componentInnards.height,
574                                    componentInnards.width);
575            if (r.height < 0) {
576                r = null;
577            } else {
578                r.width = componentInnards.width;
579                r.x = componentInnards.x;
580            }
581        } // end of VERTICAL
582
583        return r;
584    }
585
586    /**
587     * Returns the length
588     * of the "bouncing box" to be painted.
589     * This method is invoked by the
590     * default implementation of <code>paintIndeterminate</code>
591     * to get the width (if the progress bar is horizontal)
592     * or height (if vertical) of the box.
593     * For example:
594     * <blockquote>
595     * <pre>
596     *boxRect.width = getBoxLength(componentInnards.width,
597     *                             componentInnards.height);
598     * </pre>
599     * </blockquote>
600     *
601     * @param availableLength  the amount of space available
602     *                         for the bouncing box to move in;
603     *                         for a horizontal progress bar,
604     *                         for example,
605     *                         this should be
606     *                         the inside width of the progress bar
607     *                         (the component width minus borders)
608     * @param otherDimension   for a horizontal progress bar, this should be
609     *                         the inside height of the progress bar; this
610     *                         value might be used to constrain or determine
611     *                         the return value
612     *
613     * @return the size of the box dimension being determined;
614     *         must be no larger than <code>availableLength</code>
615     *
616     * @see javax.swing.SwingUtilities#calculateInnerArea
617     * @since 1.5
618     */
619    protected int getBoxLength(int availableLength, int otherDimension) {
620        return (int)Math.round(availableLength/6.0);
621    }
622
623    /**
624     * All purpose paint method that should do the right thing for all
625     * linear bouncing-box progress bars.
626     * Override this if you are making another kind of
627     * progress bar.
628     *
629     * @param g an instance of {@code Graphics}
630     * @param c a component
631     * @see #paintDeterminate
632     *
633     * @since 1.4
634     */
635    protected void paintIndeterminate(Graphics g, JComponent c) {
636        if (!(g instanceof Graphics2D)) {
637            return;
638        }
639
640        Insets b = progressBar.getInsets(); // area for border
641        int barRectWidth = progressBar.getWidth() - (b.right + b.left);
642        int barRectHeight = progressBar.getHeight() - (b.top + b.bottom);
643
644        if (barRectWidth <= 0 || barRectHeight <= 0) {
645            return;
646        }
647
648        Graphics2D g2 = (Graphics2D)g;
649
650        // Paint the bouncing box.
651        boxRect = getBox(boxRect);
652        if (boxRect != null) {
653            g2.setColor(progressBar.getForeground());
654            g2.fillRect(boxRect.x, boxRect.y,
655                       boxRect.width, boxRect.height);
656        }
657
658        // Deal with possible text painting
659        if (progressBar.isStringPainted()) {
660            if (progressBar.getOrientation() == JProgressBar.HORIZONTAL) {
661                paintString(g2, b.left, b.top,
662                            barRectWidth, barRectHeight,
663                            boxRect.x, boxRect.width, b);
664            }
665            else {
666                paintString(g2, b.left, b.top,
667                            barRectWidth, barRectHeight,
668                            boxRect.y, boxRect.height, b);
669            }
670        }
671    }
672
673
674    /**
675     * All purpose paint method that should do the right thing for almost
676     * all linear, determinate progress bars. By setting a few values in
677     * the defaults
678     * table, things should work just fine to paint your progress bar.
679     * Naturally, override this if you are making a circular or
680     * semi-circular progress bar.
681     *
682     * @param g an instance of {@code Graphics}
683     * @param c a component
684     * @see #paintIndeterminate
685     *
686     * @since 1.4
687     */
688    protected void paintDeterminate(Graphics g, JComponent c) {
689        if (!(g instanceof Graphics2D)) {
690            return;
691        }
692
693        Insets b = progressBar.getInsets(); // area for border
694        int barRectWidth = progressBar.getWidth() - (b.right + b.left);
695        int barRectHeight = progressBar.getHeight() - (b.top + b.bottom);
696
697        if (barRectWidth <= 0 || barRectHeight <= 0) {
698            return;
699        }
700
701        int cellLength = getCellLength();
702        int cellSpacing = getCellSpacing();
703        // amount of progress to draw
704        int amountFull = getAmountFull(b, barRectWidth, barRectHeight);
705
706        Graphics2D g2 = (Graphics2D)g;
707        g2.setColor(progressBar.getForeground());
708
709        if (progressBar.getOrientation() == JProgressBar.HORIZONTAL) {
710            // draw the cells
711            if (cellSpacing == 0 && amountFull > 0) {
712                // draw one big Rect because there is no space between cells
713                g2.setStroke(new BasicStroke((float)barRectHeight,
714                        BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
715            } else {
716                // draw each individual cell
717                g2.setStroke(new BasicStroke((float)barRectHeight,
718                        BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL,
719                        0.f, new float[] { cellLength, cellSpacing }, 0.f));
720            }
721
722            if (BasicGraphicsUtils.isLeftToRight(c)) {
723                g2.drawLine(b.left, (barRectHeight/2) + b.top,
724                        amountFull + b.left, (barRectHeight/2) + b.top);
725            } else {
726                g2.drawLine((barRectWidth + b.left),
727                        (barRectHeight/2) + b.top,
728                        barRectWidth + b.left - amountFull,
729                        (barRectHeight/2) + b.top);
730            }
731
732        } else { // VERTICAL
733            // draw the cells
734            if (cellSpacing == 0 && amountFull > 0) {
735                // draw one big Rect because there is no space between cells
736                g2.setStroke(new BasicStroke((float)barRectWidth,
737                        BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
738            } else {
739                // draw each individual cell
740                g2.setStroke(new BasicStroke((float)barRectWidth,
741                        BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL,
742                        0f, new float[] { cellLength, cellSpacing }, 0f));
743            }
744
745            g2.drawLine(barRectWidth/2 + b.left,
746                    b.top + barRectHeight,
747                    barRectWidth/2 + b.left,
748                    b.top + barRectHeight - amountFull);
749        }
750
751        // Deal with possible text painting
752        if (progressBar.isStringPainted()) {
753            paintString(g, b.left, b.top,
754                        barRectWidth, barRectHeight,
755                        amountFull, b);
756        }
757    }
758
759    /**
760     * Paints the progress string.
761     *
762     * @param g an instance of {@code Graphics}
763     * @param x X location of bounding box
764     * @param y Y location of bounding box
765     * @param width width of bounding box
766     * @param height height of bounding box
767     * @param amountFull size of the fill region, either width or height
768     *        depending upon orientation.
769     * @param b Insets of the progress bar.
770     */
771    protected void paintString(Graphics g, int x, int y,
772                               int width, int height,
773                               int amountFull, Insets b) {
774        if (progressBar.getOrientation() == JProgressBar.HORIZONTAL) {
775            if (BasicGraphicsUtils.isLeftToRight(progressBar)) {
776                if (progressBar.isIndeterminate()) {
777                    boxRect = getBox(boxRect);
778                    paintString(g, x, y, width, height,
779                            boxRect.x, boxRect.width, b);
780                } else {
781                    paintString(g, x, y, width, height, x, amountFull, b);
782                }
783            }
784            else {
785                paintString(g, x, y, width, height, x + width - amountFull,
786                            amountFull, b);
787            }
788        }
789        else {
790            if (progressBar.isIndeterminate()) {
791                boxRect = getBox(boxRect);
792                paintString(g, x, y, width, height,
793                        boxRect.y, boxRect.height, b);
794            } else {
795                paintString(g, x, y, width, height, y + height - amountFull,
796                        amountFull, b);
797            }
798        }
799    }
800
801    /**
802     * Paints the progress string.
803     *
804     * @param g Graphics used for drawing.
805     * @param x x location of bounding box
806     * @param y y location of bounding box
807     * @param width width of bounding box
808     * @param height height of bounding box
809     * @param fillStart start location, in x or y depending on orientation,
810     *        of the filled portion of the progress bar.
811     * @param amountFull size of the fill region, either width or height
812     *        depending upon orientation.
813     * @param b Insets of the progress bar.
814     */
815    private void paintString(Graphics g, int x, int y, int width, int height,
816                             int fillStart, int amountFull, Insets b) {
817        if (!(g instanceof Graphics2D)) {
818            return;
819        }
820
821        Graphics2D g2 = (Graphics2D)g;
822        String progressString = progressBar.getString();
823        g2.setFont(progressBar.getFont());
824        Point renderLocation = getStringPlacement(g2, progressString,
825                                                  x, y, width, height);
826        Rectangle oldClip = g2.getClipBounds();
827
828        if (progressBar.getOrientation() == JProgressBar.HORIZONTAL) {
829            g2.setColor(getSelectionBackground());
830            SwingUtilities2.drawString(progressBar, g2, progressString,
831                                       renderLocation.x, renderLocation.y);
832            g2.setColor(getSelectionForeground());
833            g2.clipRect(fillStart, y, amountFull, height);
834            SwingUtilities2.drawString(progressBar, g2, progressString,
835                                       renderLocation.x, renderLocation.y);
836        } else { // VERTICAL
837            g2.setColor(getSelectionBackground());
838            AffineTransform rotate =
839                    AffineTransform.getRotateInstance(Math.PI/2);
840            g2.setFont(progressBar.getFont().deriveFont(rotate));
841            renderLocation = getStringPlacement(g2, progressString,
842                                                  x, y, width, height);
843            SwingUtilities2.drawString(progressBar, g2, progressString,
844                                       renderLocation.x, renderLocation.y);
845            g2.setColor(getSelectionForeground());
846            g2.clipRect(x, fillStart, width, amountFull);
847            SwingUtilities2.drawString(progressBar, g2, progressString,
848                                       renderLocation.x, renderLocation.y);
849        }
850        g2.setClip(oldClip);
851    }
852
853
854    /**
855     * Designate the place where the progress string will be painted.
856     * This implementation places it at the center of the progress
857     * bar (in both x and y). Override this if you want to right,
858     * left, top, or bottom align the progress string or if you need
859     * to nudge it around for any reason.
860     *
861     * @param g an instance of {@code Graphics}
862     * @param progressString a text
863     * @param x an X coordinate
864     * @param y an Y coordinate
865     * @param width a width
866     * @param height a height
867     * @return the place where the progress string will be painted
868     */
869    protected Point getStringPlacement(Graphics g, String progressString,
870                                       int x,int y,int width,int height) {
871        FontMetrics fontSizer = SwingUtilities2.getFontMetrics(progressBar, g,
872                                            progressBar.getFont());
873        int stringWidth = SwingUtilities2.stringWidth(progressBar, fontSizer,
874                                                      progressString);
875
876        if (progressBar.getOrientation() == JProgressBar.HORIZONTAL) {
877            return new Point(x + Math.round(width/2 - stringWidth/2),
878                             y + ((height +
879                                 fontSizer.getAscent() -
880                                 fontSizer.getLeading() -
881                                 fontSizer.getDescent()) / 2));
882        } else { // VERTICAL
883            return new Point(x + ((width - fontSizer.getAscent() +
884                    fontSizer.getLeading() + fontSizer.getDescent()) / 2),
885                    y + Math.round(height/2 - stringWidth/2));
886        }
887    }
888
889
890    public Dimension getPreferredSize(JComponent c) {
891        Dimension       size;
892        Insets          border = progressBar.getInsets();
893        FontMetrics     fontSizer = progressBar.getFontMetrics(
894                                                  progressBar.getFont());
895
896        if (progressBar.getOrientation() == JProgressBar.HORIZONTAL) {
897            size = new Dimension(getPreferredInnerHorizontal());
898            // Ensure that the progress string will fit
899            if (progressBar.isStringPainted()) {
900                // I'm doing this for completeness.
901                String progString = progressBar.getString();
902                int stringWidth = SwingUtilities2.stringWidth(
903                          progressBar, fontSizer, progString);
904                if (stringWidth > size.width) {
905                    size.width = stringWidth;
906                }
907                // This uses both Height and Descent to be sure that
908                // there is more than enough room in the progress bar
909                // for everything.
910                // This does have a strange dependency on
911                // getStringPlacememnt() in a funny way.
912                int stringHeight = fontSizer.getHeight() +
913                                   fontSizer.getDescent();
914                if (stringHeight > size.height) {
915                    size.height = stringHeight;
916                }
917            }
918        } else {
919            size = new Dimension(getPreferredInnerVertical());
920            // Ensure that the progress string will fit.
921            if (progressBar.isStringPainted()) {
922                String progString = progressBar.getString();
923                int stringHeight = fontSizer.getHeight() +
924                        fontSizer.getDescent();
925                if (stringHeight > size.width) {
926                    size.width = stringHeight;
927                }
928                // This is also for completeness.
929                int stringWidth = SwingUtilities2.stringWidth(
930                                       progressBar, fontSizer, progString);
931                if (stringWidth > size.height) {
932                    size.height = stringWidth;
933                }
934            }
935        }
936
937        size.width += border.left + border.right;
938        size.height += border.top + border.bottom;
939        return size;
940    }
941
942    /**
943     * The Minimum size for this component is 10. The rationale here
944     * is that there should be at least one pixel per 10 percent.
945     */
946    public Dimension getMinimumSize(JComponent c) {
947        Dimension pref = getPreferredSize(progressBar);
948        if (progressBar.getOrientation() == JProgressBar.HORIZONTAL) {
949            pref.width = 10;
950        } else {
951            pref.height = 10;
952        }
953        return pref;
954    }
955
956    public Dimension getMaximumSize(JComponent c) {
957        Dimension pref = getPreferredSize(progressBar);
958        if (progressBar.getOrientation() == JProgressBar.HORIZONTAL) {
959            pref.width = Short.MAX_VALUE;
960        } else {
961            pref.height = Short.MAX_VALUE;
962        }
963        return pref;
964    }
965
966    /**
967     * Gets the index of the current animation frame.
968     *
969     * @return the index of the current animation frame
970     * @since 1.4
971     */
972    protected int getAnimationIndex() {
973        return animationIndex;
974    }
975
976    /**
977     * Returns the number of frames for the complete animation loop
978     * used by an indeterminate JProgessBar. The progress chunk will go
979     * from one end to the other and back during the entire loop. This
980     * visual behavior may be changed by subclasses in other Look and Feels.
981     *
982     * @return the number of frames
983     * @since 1.6
984     */
985    protected final int getFrameCount() {
986        return numFrames;
987    }
988
989    /**
990     * Sets the index of the current animation frame
991     * to the specified value and requests that the
992     * progress bar be repainted.
993     * Subclasses that don't use the default painting code
994     * might need to override this method
995     * to change the way that the <code>repaint</code> method
996     * is invoked.
997     *
998     * @param newValue the new animation index; no checking
999     *                 is performed on its value
1000     * @see #incrementAnimationIndex
1001     *
1002     * @since 1.4
1003     */
1004    protected void setAnimationIndex(int newValue) {
1005        if (animationIndex != newValue) {
1006            if (sizeChanged()) {
1007                animationIndex = newValue;
1008                maxPosition = 0;  //needs to be recalculated
1009                delta = 0.0;      //needs to be recalculated
1010                progressBar.repaint();
1011                return;
1012            }
1013
1014            //Get the previous box drawn.
1015            nextPaintRect = getBox(nextPaintRect);
1016
1017            //Update the frame number.
1018            animationIndex = newValue;
1019
1020            //Get the next box to draw.
1021            if (nextPaintRect != null) {
1022                boxRect = getBox(boxRect);
1023                if (boxRect != null) {
1024                    nextPaintRect.add(boxRect);
1025                }
1026            }
1027        } else { //animationIndex == newValue
1028            return;
1029        }
1030
1031        if (nextPaintRect != null) {
1032            progressBar.repaint(nextPaintRect);
1033        } else {
1034            progressBar.repaint();
1035        }
1036    }
1037
1038    private boolean sizeChanged() {
1039        if ((oldComponentInnards == null) || (componentInnards == null)) {
1040            return true;
1041        }
1042
1043        oldComponentInnards.setRect(componentInnards);
1044        componentInnards = SwingUtilities.calculateInnerArea(progressBar,
1045                                                             componentInnards);
1046        return !oldComponentInnards.equals(componentInnards);
1047    }
1048
1049    /**
1050     * Sets the index of the current animation frame,
1051     * to the next valid value,
1052     * which results in the progress bar being repainted.
1053     * The next valid value is, by default,
1054     * the current animation index plus one.
1055     * If the new value would be too large,
1056     * this method sets the index to 0.
1057     * Subclasses might need to override this method
1058     * to ensure that the index does not go over
1059     * the number of frames needed for the particular
1060     * progress bar instance.
1061     * This method is invoked by the default animation thread
1062     * every <em>X</em> milliseconds,
1063     * where <em>X</em> is specified by the "ProgressBar.repaintInterval"
1064     * UI default.
1065     *
1066     * @see #setAnimationIndex
1067     * @since 1.4
1068     */
1069    protected void incrementAnimationIndex() {
1070        int newValue = getAnimationIndex() + 1;
1071
1072        if (newValue < numFrames) {
1073            setAnimationIndex(newValue);
1074        } else {
1075            setAnimationIndex(0);
1076        }
1077    }
1078
1079    /**
1080     * Returns the desired number of milliseconds between repaints.
1081     * This value is meaningful
1082     * only if the progress bar is in indeterminate mode.
1083     * The repaint interval determines how often the
1084     * default animation thread's timer is fired.
1085     * It's also used by the default indeterminate progress bar
1086     * painting code when determining
1087     * how far to move the bouncing box per frame.
1088     * The repaint interval is specified by
1089     * the "ProgressBar.repaintInterval" UI default.
1090     *
1091     * @return  the repaint interval, in milliseconds
1092     */
1093    private int getRepaintInterval() {
1094        return repaintInterval;
1095    }
1096
1097    private int initRepaintInterval() {
1098        repaintInterval = DefaultLookup.getInt(progressBar,
1099                this, "ProgressBar.repaintInterval", 50);
1100        return repaintInterval;
1101    }
1102
1103    /**
1104     * Returns the number of milliseconds per animation cycle.
1105     * This value is meaningful
1106     * only if the progress bar is in indeterminate mode.
1107     * The cycle time is used by the default indeterminate progress bar
1108     * painting code when determining
1109     * how far to move the bouncing box per frame.
1110     * The cycle time is specified by
1111     * the "ProgressBar.cycleTime" UI default
1112     * and adjusted, if necessary,
1113     * by the initIndeterminateDefaults method.
1114     *
1115     * @return  the cycle time, in milliseconds
1116     */
1117    private int getCycleTime() {
1118        return cycleTime;
1119    }
1120
1121    private int initCycleTime() {
1122        cycleTime = DefaultLookup.getInt(progressBar, this,
1123                "ProgressBar.cycleTime", 3000);
1124        return cycleTime;
1125    }
1126
1127
1128    /** Initialize cycleTime, repaintInterval, numFrames, animationIndex. */
1129    private void initIndeterminateDefaults() {
1130        initRepaintInterval(); //initialize repaint interval
1131        initCycleTime();       //initialize cycle length
1132
1133        // Make sure repaintInterval is reasonable.
1134        if (repaintInterval <= 0) {
1135            repaintInterval = 100;
1136        }
1137
1138        // Make sure cycleTime is reasonable.
1139        if (repaintInterval > cycleTime) {
1140            cycleTime = repaintInterval * 20;
1141        } else {
1142            // Force cycleTime to be a even multiple of repaintInterval.
1143            int factor = (int)Math.ceil(
1144                                 ((double)cycleTime)
1145                               / ((double)repaintInterval*2));
1146            cycleTime = repaintInterval*factor*2;
1147        }
1148    }
1149
1150    /**
1151     * Invoked by PropertyChangeHandler.
1152     *
1153     *  NOTE: This might not be invoked until after the first
1154     *  paintIndeterminate call.
1155     */
1156    private void initIndeterminateValues() {
1157        initIndeterminateDefaults();
1158        //assert cycleTime/repaintInterval is a whole multiple of 2.
1159        numFrames = cycleTime/repaintInterval;
1160        initAnimationIndex();
1161
1162        boxRect = new Rectangle();
1163        nextPaintRect = new Rectangle();
1164        componentInnards = new Rectangle();
1165        oldComponentInnards = new Rectangle();
1166
1167        // we only bother installing the HierarchyChangeListener if we
1168        // are indeterminate
1169        progressBar.addHierarchyListener(getHandler());
1170
1171        // start the animation thread if necessary
1172        if (progressBar.isDisplayable()) {
1173            startAnimationTimer();
1174        }
1175    }
1176
1177    /** Invoked by PropertyChangeHandler. */
1178    private void cleanUpIndeterminateValues() {
1179        // stop the animation thread if necessary
1180        if (progressBar.isDisplayable()) {
1181            stopAnimationTimer();
1182        }
1183
1184        cycleTime = repaintInterval = 0;
1185        numFrames = animationIndex = 0;
1186        maxPosition = 0;
1187        delta = 0.0;
1188
1189        boxRect = nextPaintRect = null;
1190        componentInnards = oldComponentInnards = null;
1191
1192        progressBar.removeHierarchyListener(getHandler());
1193    }
1194
1195    // Called from initIndeterminateValues to initialize the animation index.
1196    // This assumes that numFrames is set to a correct value.
1197    private void initAnimationIndex() {
1198        if ((progressBar.getOrientation() == JProgressBar.HORIZONTAL) &&
1199            (BasicGraphicsUtils.isLeftToRight(progressBar))) {
1200            // If this is a left-to-right progress bar,
1201            // start at the first frame.
1202            setAnimationIndex(0);
1203        } else {
1204            // If we go right-to-left or vertically, start at the right/bottom.
1205            setAnimationIndex(numFrames/2);
1206        }
1207    }
1208
1209    //
1210    // Animation Thread
1211    //
1212    /**
1213     * Implements an animation thread that invokes repaint
1214     * at a fixed rate.  If ADJUSTTIMER is true, this thread
1215     * will continuously adjust the repaint interval to
1216     * try to make the actual time between repaints match
1217     * the requested rate.
1218     */
1219    private class Animator implements ActionListener {
1220        private Timer timer;
1221        private long previousDelay; //used to tune the repaint interval
1222        private int interval; //the fixed repaint interval
1223        private long lastCall; //the last time actionPerformed was called
1224        private int MINIMUM_DELAY = 5;
1225
1226        /**
1227         * Creates a timer if one doesn't already exist,
1228         * then starts the timer thread.
1229         */
1230        private void start(int interval) {
1231            previousDelay = interval;
1232            lastCall = 0;
1233
1234            if (timer == null) {
1235                timer = new Timer(interval, this);
1236            } else {
1237                timer.setDelay(interval);
1238            }
1239
1240            if (ADJUSTTIMER) {
1241                timer.setRepeats(false);
1242                timer.setCoalesce(false);
1243            }
1244
1245            timer.start();
1246        }
1247
1248        /**
1249         * Stops the timer thread.
1250         */
1251        private void stop() {
1252            timer.stop();
1253        }
1254
1255        /**
1256         * Reacts to the timer's action events.
1257         */
1258        public void actionPerformed(ActionEvent e) {
1259            if (ADJUSTTIMER) {
1260                long time = System.currentTimeMillis();
1261
1262                if (lastCall > 0) { //adjust nextDelay
1263                //XXX maybe should cache this after a while
1264                    //actual = time - lastCall
1265                    //difference = actual - interval
1266                    //nextDelay = previousDelay - difference
1267                    //          = previousDelay - (time - lastCall - interval)
1268                   int nextDelay = (int)(previousDelay
1269                                          - time + lastCall
1270                                          + getRepaintInterval());
1271                    if (nextDelay < MINIMUM_DELAY) {
1272                        nextDelay = MINIMUM_DELAY;
1273                    }
1274                    timer.setInitialDelay(nextDelay);
1275                    previousDelay = nextDelay;
1276                }
1277                timer.start();
1278                lastCall = time;
1279            }
1280
1281            incrementAnimationIndex(); //paint next frame
1282        }
1283    }
1284
1285
1286    /**
1287     * This class should be treated as a &quot;protected&quot; inner class.
1288     * Instantiate it only within subclasses of {@code BasicProgressBarUI}.
1289     */
1290    public class ChangeHandler implements ChangeListener {
1291        // NOTE: This class exists only for backward compatibility. All
1292        // its functionality has been moved into Handler. If you need to add
1293        // new functionality add it to the Handler, but make sure this
1294        // class calls into the Handler.
1295        public void stateChanged(ChangeEvent e) {
1296            getHandler().stateChanged(e);
1297        }
1298    }
1299
1300
1301    private class Handler implements ChangeListener, PropertyChangeListener, HierarchyListener {
1302        // ChangeListener
1303        public void stateChanged(ChangeEvent e) {
1304            BoundedRangeModel model = progressBar.getModel();
1305            int newRange = model.getMaximum() - model.getMinimum();
1306            int newPercent;
1307            int oldPercent = getCachedPercent();
1308
1309            if (newRange > 0) {
1310                newPercent = (int)((100 * (long)model.getValue()) / newRange);
1311            } else {
1312                newPercent = 0;
1313            }
1314
1315            if (newPercent != oldPercent) {
1316                setCachedPercent(newPercent);
1317                progressBar.repaint();
1318            }
1319        }
1320
1321        // PropertyChangeListener
1322        public void propertyChange(PropertyChangeEvent e) {
1323            String prop = e.getPropertyName();
1324            if ("indeterminate" == prop) {
1325                if (progressBar.isIndeterminate()) {
1326                    initIndeterminateValues();
1327                } else {
1328                    //clean up
1329                    cleanUpIndeterminateValues();
1330                }
1331                progressBar.repaint();
1332            }
1333        }
1334
1335        // we don't want the animation to keep running if we're not displayable
1336        public void hierarchyChanged(HierarchyEvent he) {
1337            if ((he.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED) != 0) {
1338                if (progressBar.isIndeterminate()) {
1339                    if (progressBar.isDisplayable()) {
1340                        startAnimationTimer();
1341                    } else {
1342                        stopAnimationTimer();
1343                    }
1344                }
1345            }
1346        }
1347    }
1348}
1349