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 
26 package com.apple.laf;
27 
28 import java.awt.*;
29 import java.awt.event.*;
30 import java.awt.geom.AffineTransform;
31 import java.beans.*;
32 
33 import javax.swing.*;
34 import javax.swing.event.*;
35 import javax.swing.plaf.*;
36 
37 import sun.swing.SwingUtilities2;
38 
39 import apple.laf.JRSUIStateFactory;
40 import apple.laf.JRSUIConstants.*;
41 import apple.laf.JRSUIState.ValueState;
42 
43 import com.apple.laf.AquaUtilControlSize.*;
44 import com.apple.laf.AquaUtils.RecyclableSingleton;
45 
46 public 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     };
getSizeDescriptor()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 
createUI(final JComponent x)73     public static ComponentUI createUI(final JComponent x) {
74         return new AquaProgressBarUI();
75     }
76 
AquaProgressBarUI()77     protected AquaProgressBarUI() { }
78 
installUI(final JComponent c)79     public void installUI(final JComponent c) {
80         progressBar = (JProgressBar)c;
81         installDefaults();
82         installListeners();
83     }
84 
uninstallUI(final JComponent c)85     public void uninstallUI(final JComponent c) {
86         uninstallDefaults();
87         uninstallListeners();
88         stopAnimationTimer();
89         progressBar = null;
90     }
91 
installDefaults()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 
uninstallDefaults()99     protected void uninstallDefaults() {
100         LookAndFeel.uninstallBorder(progressBar);
101     }
102 
installListeners()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 
uninstallListeners()110     protected void uninstallListeners() {
111         AquaUtilControlSize.removeSizePropertyListener(progressBar);
112         progressBar.removeAncestorListener(this);
113         progressBar.removePropertyChangeListener(this);
114         progressBar.removeChangeListener(this);
115     }
116 
stateChanged(final ChangeEvent e)117     public void stateChanged(final ChangeEvent e) {
118         progressBar.repaint();
119     }
120 
propertyChange(final PropertyChangeEvent e)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
ancestorRemoved(final AncestorEvent e)140     public void ancestorRemoved(final AncestorEvent e) {
141         stopAnimationTimer();
142     }
143 
ancestorAdded(final AncestorEvent e)144     public void ancestorAdded(final AncestorEvent e) {
145         if (!progressBar.isIndeterminate()) return;
146         if (progressBar.isDisplayable()) {
147           startAnimationTimer();
148         }
149     }
150 
ancestorMoved(final AncestorEvent e)151     public void ancestorMoved(final AncestorEvent e) { }
152 
paint(final Graphics g, final JComponent c)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 
checkValue(final double value)177     static double checkValue(final double value) {
178         return Double.isNaN(value) ? 0 : value;
179     }
180 
paint(final Graphics g)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 
getState(final JComponent c)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 
paintString(final Graphics g, final int x, final int y, final int width, final int height)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      */
getStringPlacement(final Graphics g, final String progressString, int x, int y, int width, int height)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 
getCircularPreferredSize()262     static Dimension getCircularPreferredSize() {
263         return new Dimension(20, 20);
264     }
265 
getPreferredSize(final JComponent c)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 
getPreferredHorizontalSize(final FontMetrics metrics)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 
getPreferredVerticalSize(final FontMetrics metrics)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 
getMinimumSize(final JComponent c)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 
getMaximumSize(final JComponent c)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 
applySizeFor(final JComponent c, final Size size)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 
startAnimationTimer()363     protected void startAnimationTimer() {
364         if (animator == null) animator = new Animator();
365         animator.start();
366         isAnimating = true;
367     }
368 
stopAnimationTimer()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);
getRepaintRect()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 
getMaxProgressBarHeight()410     protected int getMaxProgressBarHeight() {
411         return getSizeDescriptor().get(sizeVariant).h;
412     }
413 
isHorizontal()414     protected boolean isHorizontal() {
415         return progressBar.getOrientation() == SwingConstants.HORIZONTAL;
416     }
417 
revalidateAnimationTimers()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 
repaint()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 
Animator()450         public Animator() {
451             repaintInterval = UIManager.getInt("ProgressBar.repaintInterval");
452 
453             // Make sure repaintInterval is reasonable.
454             if (repaintInterval <= 0) repaintInterval = 100;
455         }
456 
start()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 
stop()475         protected void stop() {
476             timer.stop();
477         }
478 
actionPerformed(final ActionEvent e)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