1/*
2 * Copyright (c) 2011, 2012, 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 com.apple.laf;
27
28import java.awt.*;
29import java.awt.event.*;
30import java.awt.geom.AffineTransform;
31import java.beans.*;
32
33import javax.swing.*;
34import javax.swing.event.*;
35import javax.swing.plaf.*;
36
37import sun.swing.SwingUtilities2;
38
39import apple.laf.JRSUIStateFactory;
40import apple.laf.JRSUIConstants.*;
41import apple.laf.JRSUIState.ValueState;
42
43import com.apple.laf.AquaUtilControlSize.*;
44import com.apple.laf.AquaUtils.RecyclableSingleton;
45
46public class AquaProgressBarUI extends ProgressBarUI implements ChangeListener, PropertyChangeListener, AncestorListener, Sizeable {
47    private static final boolean ADJUSTTIMER = true;
48
49    private static final RecyclableSingleton<SizeDescriptor> sizeDescriptor = new RecyclableSingleton<SizeDescriptor>() {
50        @Override
51        protected SizeDescriptor getInstance() {
52            return new SizeDescriptor(new SizeVariant(146, 20)) {
53                public SizeVariant deriveSmall(final SizeVariant v) { v.alterMinSize(0, -6); return super.deriveSmall(v); }
54            };
55        }
56    };
57    static SizeDescriptor getSizeDescriptor() {
58        return sizeDescriptor.get();
59    }
60
61    protected Size sizeVariant = Size.REGULAR;
62
63    protected Color selectionForeground;
64
65    private Animator animator;
66    protected boolean isAnimating;
67    protected boolean isCircular;
68
69    protected final AquaPainter<ValueState> painter = AquaPainter.create(JRSUIStateFactory.getProgressBar());
70
71    protected JProgressBar progressBar;
72
73    public static ComponentUI createUI(final JComponent x) {
74        return new AquaProgressBarUI();
75    }
76
77    protected AquaProgressBarUI() { }
78
79    public void installUI(final JComponent c) {
80        progressBar = (JProgressBar)c;
81        installDefaults();
82        installListeners();
83    }
84
85    public void uninstallUI(final JComponent c) {
86        uninstallDefaults();
87        uninstallListeners();
88        stopAnimationTimer();
89        progressBar = null;
90    }
91
92    protected void installDefaults() {
93        progressBar.setOpaque(false);
94        LookAndFeel.installBorder(progressBar, "ProgressBar.border");
95        LookAndFeel.installColorsAndFont(progressBar, "ProgressBar.background", "ProgressBar.foreground", "ProgressBar.font");
96        selectionForeground = UIManager.getColor("ProgressBar.selectionForeground");
97    }
98
99    protected void uninstallDefaults() {
100        LookAndFeel.uninstallBorder(progressBar);
101    }
102
103    protected void installListeners() {
104        progressBar.addChangeListener(this); // Listen for changes in the progress bar's data
105        progressBar.addPropertyChangeListener(this); // Listen for changes between determinate and indeterminate state
106        progressBar.addAncestorListener(this);
107        AquaUtilControlSize.addSizePropertyListener(progressBar);
108    }
109
110    protected void uninstallListeners() {
111        AquaUtilControlSize.removeSizePropertyListener(progressBar);
112        progressBar.removeAncestorListener(this);
113        progressBar.removePropertyChangeListener(this);
114        progressBar.removeChangeListener(this);
115    }
116
117    public void stateChanged(final ChangeEvent e) {
118        progressBar.repaint();
119    }
120
121    public void propertyChange(final PropertyChangeEvent e) {
122        final String prop = e.getPropertyName();
123        if ("indeterminate".equals(prop)) {
124            if (!progressBar.isIndeterminate()) return;
125            stopAnimationTimer();
126            // start the animation thread
127            if (progressBar.isDisplayable()) {
128              startAnimationTimer();
129            }
130        }
131
132        if ("JProgressBar.style".equals(prop)) {
133            isCircular = "circular".equalsIgnoreCase(e.getNewValue() + "");
134            progressBar.repaint();
135        }
136    }
137
138    // listen for Ancestor events to stop our timer when we are no longer visible
139    // <rdar://problem/5405035> JProgressBar: UI in Aqua look and feel causes memory leaks
140    public void ancestorRemoved(final AncestorEvent e) {
141        stopAnimationTimer();
142    }
143
144    public void ancestorAdded(final AncestorEvent e) {
145        if (!progressBar.isIndeterminate()) return;
146        if (progressBar.isDisplayable()) {
147          startAnimationTimer();
148        }
149    }
150
151    public void ancestorMoved(final AncestorEvent e) { }
152
153    public void paint(final Graphics g, final JComponent c) {
154        revalidateAnimationTimers(); // revalidate to turn on/off timers when values change
155
156        painter.state.set(getState(c));
157        painter.state.set(isHorizontal() ? Orientation.HORIZONTAL : Orientation.VERTICAL);
158        painter.state.set(isAnimating ? Animating.YES : Animating.NO);
159
160        if (progressBar.isIndeterminate()) {
161            if (isCircular) {
162                painter.state.set(Widget.PROGRESS_SPINNER);
163                painter.paint(g, c, 2, 2, 16, 16);
164                return;
165            }
166
167            painter.state.set(Widget.PROGRESS_INDETERMINATE_BAR);
168            paint(g);
169            return;
170        }
171
172        painter.state.set(Widget.PROGRESS_BAR);
173        painter.state.setValue(checkValue(progressBar.getPercentComplete()));
174        paint(g);
175    }
176
177    static double checkValue(final double value) {
178        return Double.isNaN(value) ? 0 : value;
179    }
180
181    protected void paint(final Graphics g) {
182        // this is questionable. We may want the insets to mean something different.
183        final Insets i = progressBar.getInsets();
184        final int width = progressBar.getWidth() - (i.right + i.left);
185        final int height = progressBar.getHeight() - (i.bottom + i.top);
186
187        Graphics2D g2 = (Graphics2D) g;
188        final AffineTransform savedAT = g2.getTransform();
189        if (!progressBar.getComponentOrientation().isLeftToRight()) {
190            //Scale operation: Flips component about pivot
191            //Translate operation: Moves component back into original position
192            g2.scale(-1, 1);
193            g2.translate(-progressBar.getWidth(), 0);
194        }
195        painter.paint(g, progressBar, i.left, i.top, width, height);
196
197        g2.setTransform(savedAT);
198        if (progressBar.isStringPainted() && !progressBar.isIndeterminate()) {
199                paintString(g, i.left, i.top, width, height);
200        }
201    }
202
203    protected State getState(final JComponent c) {
204        if (!c.isEnabled()) return State.INACTIVE;
205        if (!AquaFocusHandler.isActive(c)) return State.INACTIVE;
206        return State.ACTIVE;
207    }
208
209    protected void paintString(final Graphics g, final int x, final int y, final int width, final int height) {
210        if (!(g instanceof Graphics2D)) return;
211
212        final Graphics2D g2 = (Graphics2D)g;
213        final String progressString = progressBar.getString();
214        g2.setFont(progressBar.getFont());
215        final Point renderLocation = getStringPlacement(g2, progressString, x, y, width, height);
216        final Rectangle oldClip = g2.getClipBounds();
217
218        if (isHorizontal()) {
219            g2.setColor(selectionForeground);
220            SwingUtilities2.drawString(progressBar, g2, progressString, renderLocation.x, renderLocation.y);
221        } else { // VERTICAL
222            // We rotate it -90 degrees, then translate it down since we are going to be bottom up.
223            final AffineTransform savedAT = g2.getTransform();
224            g2.transform(AffineTransform.getRotateInstance(0.0f - (Math.PI / 2.0f), 0, 0));
225            g2.translate(-progressBar.getHeight(), 0);
226
227            // 0,0 is now the bottom left of the viewable area, so we just draw our image at
228            // the render location since that calculation knows about rotation.
229            g2.setColor(selectionForeground);
230            SwingUtilities2.drawString(progressBar, g2, progressString, renderLocation.x, renderLocation.y);
231
232            g2.setTransform(savedAT);
233        }
234
235        g2.setClip(oldClip);
236    }
237
238    /**
239     * Designate the place where the progress string will be painted. This implementation places it at the center of the
240     * progress bar (in both x and y). Override this if you want to right, left, top, or bottom align the progress
241     * string or if you need to nudge it around for any reason.
242     */
243    protected Point getStringPlacement(final Graphics g, final String progressString, int x, int y, int width, int height) {
244        final FontMetrics fontSizer = progressBar.getFontMetrics(progressBar.getFont());
245        final int stringWidth = fontSizer.stringWidth(progressString);
246
247        if (!isHorizontal()) {
248            // Calculate the location for the rotated text in real component coordinates.
249            // swapping x & y and width & height
250            final int oldH = height;
251            height = width;
252            width = oldH;
253
254            final int oldX = x;
255            x = y;
256            y = oldX;
257        }
258
259        return new Point(x + Math.round(width / 2 - stringWidth / 2), y + ((height + fontSizer.getAscent() - fontSizer.getLeading() - fontSizer.getDescent()) / 2) - 1);
260    }
261
262    static Dimension getCircularPreferredSize() {
263        return new Dimension(20, 20);
264    }
265
266    public Dimension getPreferredSize(final JComponent c) {
267        if (isCircular) {
268            return getCircularPreferredSize();
269        }
270
271        final FontMetrics metrics = progressBar.getFontMetrics(progressBar.getFont());
272
273        final Dimension size = isHorizontal() ? getPreferredHorizontalSize(metrics) : getPreferredVerticalSize(metrics);
274        final Insets insets = progressBar.getInsets();
275
276        size.width += insets.left + insets.right;
277        size.height += insets.top + insets.bottom;
278        return size;
279    }
280
281    protected Dimension getPreferredHorizontalSize(final FontMetrics metrics) {
282        final SizeVariant variant = getSizeDescriptor().get(sizeVariant);
283        final Dimension size = new Dimension(variant.w, variant.h);
284        if (!progressBar.isStringPainted()) return size;
285
286        // Ensure that the progress string will fit
287        final String progString = progressBar.getString();
288        final int stringWidth = metrics.stringWidth(progString);
289        if (stringWidth > size.width) {
290            size.width = stringWidth;
291        }
292
293        // This uses both Height and Descent to be sure that
294        // there is more than enough room in the progress bar
295        // for everything.
296        // This does have a strange dependency on
297        // getStringPlacememnt() in a funny way.
298        final int stringHeight = metrics.getHeight() + metrics.getDescent();
299        if (stringHeight > size.height) {
300            size.height = stringHeight;
301        }
302        return size;
303    }
304
305    protected Dimension getPreferredVerticalSize(final FontMetrics metrics) {
306        final SizeVariant variant = getSizeDescriptor().get(sizeVariant);
307        final Dimension size = new Dimension(variant.h, variant.w);
308        if (!progressBar.isStringPainted()) return size;
309
310        // Ensure that the progress string will fit.
311        final String progString = progressBar.getString();
312        final int stringHeight = metrics.getHeight() + metrics.getDescent();
313        if (stringHeight > size.width) {
314            size.width = stringHeight;
315        }
316
317        // This is also for completeness.
318        final int stringWidth = metrics.stringWidth(progString);
319        if (stringWidth > size.height) {
320            size.height = stringWidth;
321        }
322        return size;
323    }
324
325    public Dimension getMinimumSize(final JComponent c) {
326        if (isCircular) {
327            return getCircularPreferredSize();
328        }
329
330        final Dimension pref = getPreferredSize(progressBar);
331
332        // The Minimum size for this component is 10.
333        // The rationale here is that there should be at least one pixel per 10 percent.
334        if (isHorizontal()) {
335            pref.width = 10;
336        } else {
337            pref.height = 10;
338        }
339
340        return pref;
341    }
342
343    public Dimension getMaximumSize(final JComponent c) {
344        if (isCircular) {
345            return getCircularPreferredSize();
346        }
347
348        final Dimension pref = getPreferredSize(progressBar);
349
350        if (isHorizontal()) {
351            pref.width = Short.MAX_VALUE;
352        } else {
353            pref.height = Short.MAX_VALUE;
354        }
355
356        return pref;
357    }
358
359    public void applySizeFor(final JComponent c, final Size size) {
360        painter.state.set(sizeVariant = size == Size.MINI ? Size.SMALL : sizeVariant); // CUI doesn't support mini progress bars right now
361    }
362
363    protected void startAnimationTimer() {
364        if (animator == null) animator = new Animator();
365        animator.start();
366        isAnimating = true;
367    }
368
369    protected void stopAnimationTimer() {
370        if (animator != null) animator.stop();
371        isAnimating = false;
372    }
373
374    private final Rectangle fUpdateArea = new Rectangle(0, 0, 0, 0);
375    private final Dimension fLastSize = new Dimension(0, 0);
376    protected Rectangle getRepaintRect() {
377        int height = progressBar.getHeight();
378        int width = progressBar.getWidth();
379
380        if (isCircular) {
381            return new Rectangle(20, 20);
382        }
383
384        if (fLastSize.height == height && fLastSize.width == width) {
385            return fUpdateArea;
386        }
387
388        int x = 0;
389        int y = 0;
390        fLastSize.height = height;
391        fLastSize.width = width;
392
393        final int maxHeight = getMaxProgressBarHeight();
394
395        if (isHorizontal()) {
396            final int excessHeight = height - maxHeight;
397            y += excessHeight / 2;
398            height = maxHeight;
399        } else {
400            final int excessHeight = width - maxHeight;
401            x += excessHeight / 2;
402            width = maxHeight;
403        }
404
405        fUpdateArea.setBounds(x, y, width, height);
406
407        return fUpdateArea;
408    }
409
410    protected int getMaxProgressBarHeight() {
411        return getSizeDescriptor().get(sizeVariant).h;
412    }
413
414    protected boolean isHorizontal() {
415        return progressBar.getOrientation() == SwingConstants.HORIZONTAL;
416    }
417
418    protected void revalidateAnimationTimers() {
419        if (progressBar.isIndeterminate()) return;
420
421        if (!isAnimating) {
422            startAnimationTimer(); // only starts if supposed to!
423            return;
424        }
425
426        final BoundedRangeModel model = progressBar.getModel();
427        final double currentValue = model.getValue();
428        if ((currentValue == model.getMaximum()) || (currentValue == model.getMinimum())) {
429            stopAnimationTimer();
430        }
431    }
432
433    protected void repaint() {
434        final Rectangle repaintRect = getRepaintRect();
435        if (repaintRect == null) {
436            progressBar.repaint();
437            return;
438        }
439
440        progressBar.repaint(repaintRect);
441    }
442
443    protected class Animator implements ActionListener {
444        private static final int MINIMUM_DELAY = 5;
445        private Timer timer;
446        private long previousDelay; // used to tune the repaint interval
447        private long lastCall; // the last time actionPerformed was called
448        private int repaintInterval;
449
450        public Animator() {
451            repaintInterval = UIManager.getInt("ProgressBar.repaintInterval");
452
453            // Make sure repaintInterval is reasonable.
454            if (repaintInterval <= 0) repaintInterval = 100;
455        }
456
457        protected void start() {
458            previousDelay = repaintInterval;
459            lastCall = 0;
460
461            if (timer == null) {
462                timer = new Timer(repaintInterval, this);
463            } else {
464                timer.setDelay(repaintInterval);
465            }
466
467            if (ADJUSTTIMER) {
468                timer.setRepeats(false);
469                timer.setCoalesce(false);
470            }
471
472            timer.start();
473        }
474
475        protected void stop() {
476            timer.stop();
477        }
478
479        public void actionPerformed(final ActionEvent e) {
480            if (!ADJUSTTIMER) {
481                repaint();
482                return;
483            }
484
485            final long time = System.currentTimeMillis();
486
487            if (lastCall > 0) {
488                // adjust nextDelay
489                int nextDelay = (int)(previousDelay - time + lastCall + repaintInterval);
490                if (nextDelay < MINIMUM_DELAY) {
491                    nextDelay = MINIMUM_DELAY;
492                }
493
494                timer.setInitialDelay(nextDelay);
495                previousDelay = nextDelay;
496            }
497
498            timer.start();
499            lastCall = time;
500
501            repaint();
502        }
503    }
504}
505