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     protected 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             startAnimationTimer();
 128         }
 129 
 130         if ("JProgressBar.style".equals(prop)) {
 131             isCircular = "circular".equalsIgnoreCase(e.getNewValue() + "");
 132             progressBar.repaint();
 133         }
 134     }
 135 
 136     // listen for Ancestor events to stop our timer when we are no longer visible
 137     // <rdar://problem/5405035> JProgressBar: UI in Aqua look and feel causes memory leaks
 138     public void ancestorRemoved(final AncestorEvent e) {
 139         stopAnimationTimer();
 140     }
 141 
 142     public void ancestorAdded(final AncestorEvent e) {
 143         if (!progressBar.isIndeterminate()) return;
 144         startAnimationTimer();
 145     }
 146 
 147     public void ancestorMoved(final AncestorEvent e) { }
 148 
 149     public void paint(final Graphics g, final JComponent c) {
 150         revalidateAnimationTimers(); // revalidate to turn on/off timers when values change
 151 
 152         painter.state.set(getState(c));
 153         painter.state.set(isHorizontal() ? Orientation.HORIZONTAL : Orientation.VERTICAL);
 154         painter.state.set(isAnimating ? Animating.YES : Animating.NO);
 155 
 156         if (progressBar.isIndeterminate()) {
 157             if (isCircular) {
 158                 painter.state.set(Widget.PROGRESS_SPINNER);
 159                 painter.paint(g, c, 2, 2, 16, 16);
 160                 return;
 161             }
 162 
 163             painter.state.set(Widget.PROGRESS_INDETERMINATE_BAR);
 164             paint(g);
 165             return;
 166         }
 167 
 168         painter.state.set(Widget.PROGRESS_BAR);
 169         painter.state.setValue(checkValue(progressBar.getPercentComplete()));
 170         paint(g);
 171     }
 172 
 173     static double checkValue(final double value) {
 174         return Double.isNaN(value) ? 0 : value;
 175     }
 176 
 177     protected void paint(final Graphics g) {
 178         // this is questionable. We may want the insets to mean something different.
 179         final Insets i = progressBar.getInsets();
 180         final int width = progressBar.getWidth() - (i.right + i.left);
 181         final int height = progressBar.getHeight() - (i.bottom + i.top);
 182 
 183         painter.paint(g, progressBar, i.left, i.top, width, height);
 184 
 185         if (progressBar.isStringPainted() && !progressBar.isIndeterminate()) {
 186             paintString(g, i.left, i.top, width, height);
 187         }
 188     }
 189 
 190     protected State getState(final JComponent c) {
 191         if (!c.isEnabled()) return State.INACTIVE;
 192         if (!AquaFocusHandler.isActive(c)) return State.INACTIVE;
 193         return State.ACTIVE;
 194     }
 195 
 196     protected void paintString(final Graphics g, final int x, final int y, final int width, final int height) {
 197         if (!(g instanceof Graphics2D)) return;
 198 
 199         final Graphics2D g2 = (Graphics2D)g;
 200         final String progressString = progressBar.getString();
 201         g2.setFont(progressBar.getFont());
 202         final Point renderLocation = getStringPlacement(g2, progressString, x, y, width, height);
 203         final Rectangle oldClip = g2.getClipBounds();
 204 
 205         if (isHorizontal()) {
 206             g2.setColor(selectionForeground);
 207             SwingUtilities2.drawString(progressBar, g2, progressString, renderLocation.x, renderLocation.y);
 208         } else { // VERTICAL
 209             // We rotate it -90 degrees, then translate it down since we are going to be bottom up.
 210             final AffineTransform savedAT = g2.getTransform();
 211             g2.transform(AffineTransform.getRotateInstance(0.0f - (Math.PI / 2.0f), 0, 0));
 212             g2.translate(-progressBar.getHeight(), 0);
 213 
 214             // 0,0 is now the bottom left of the viewable area, so we just draw our image at
 215             // the render location since that calculation knows about rotation.
 216             g2.setColor(selectionForeground);
 217             SwingUtilities2.drawString(progressBar, g2, progressString, renderLocation.x, renderLocation.y);
 218 
 219             g2.setTransform(savedAT);
 220         }
 221 
 222         g2.setClip(oldClip);
 223     }
 224 
 225     /**
 226      * Designate the place where the progress string will be painted. This implementation places it at the center of the
 227      * progress bar (in both x and y). Override this if you want to right, left, top, or bottom align the progress
 228      * string or if you need to nudge it around for any reason.
 229      */
 230     protected Point getStringPlacement(final Graphics g, final String progressString, int x, int y, int width, int height) {
 231         final FontMetrics fontSizer = progressBar.getFontMetrics(progressBar.getFont());
 232         final int stringWidth = fontSizer.stringWidth(progressString);
 233 
 234         if (!isHorizontal()) {
 235             // Calculate the location for the rotated text in real component coordinates.
 236             // swapping x & y and width & height
 237             final int oldH = height;
 238             height = width;
 239             width = oldH;
 240 
 241             final int oldX = x;
 242             x = y;
 243             y = oldX;
 244         }
 245 
 246         return new Point(x + Math.round(width / 2 - stringWidth / 2), y + ((height + fontSizer.getAscent() - fontSizer.getLeading() - fontSizer.getDescent()) / 2) - 1);
 247     }
 248 
 249     static Dimension getCircularPreferredSize() {
 250         return new Dimension(20, 20);
 251     }
 252 
 253     public Dimension getPreferredSize(final JComponent c) {
 254         if (isCircular) {
 255             return getCircularPreferredSize();
 256         }
 257 
 258         final FontMetrics metrics = progressBar.getFontMetrics(progressBar.getFont());
 259 
 260         final Dimension size = isHorizontal() ? getPreferredHorizontalSize(metrics) : getPreferredVerticalSize(metrics);
 261         final Insets insets = progressBar.getInsets();
 262 
 263         size.width += insets.left + insets.right;
 264         size.height += insets.top + insets.bottom;
 265         return size;
 266     }
 267 
 268     protected Dimension getPreferredHorizontalSize(final FontMetrics metrics) {
 269         final SizeVariant variant = getSizeDescriptor().get(sizeVariant);
 270         final Dimension size = new Dimension(variant.w, variant.h);
 271         if (!progressBar.isStringPainted()) return size;
 272 
 273         // Ensure that the progress string will fit
 274         final String progString = progressBar.getString();
 275         final int stringWidth = metrics.stringWidth(progString);
 276         if (stringWidth > size.width) {
 277             size.width = stringWidth;
 278         }
 279 
 280         // This uses both Height and Descent to be sure that
 281         // there is more than enough room in the progress bar
 282         // for everything.
 283         // This does have a strange dependency on
 284         // getStringPlacememnt() in a funny way.
 285         final int stringHeight = metrics.getHeight() + metrics.getDescent();
 286         if (stringHeight > size.height) {
 287             size.height = stringHeight;
 288         }
 289         return size;
 290     }
 291 
 292     protected Dimension getPreferredVerticalSize(final FontMetrics metrics) {
 293         final SizeVariant variant = getSizeDescriptor().get(sizeVariant);
 294         final Dimension size = new Dimension(variant.h, variant.w);
 295         if (!progressBar.isStringPainted()) return size;
 296 
 297         // Ensure that the progress string will fit.
 298         final String progString = progressBar.getString();
 299         final int stringHeight = metrics.getHeight() + metrics.getDescent();
 300         if (stringHeight > size.width) {
 301             size.width = stringHeight;
 302         }
 303 
 304         // This is also for completeness.
 305         final int stringWidth = metrics.stringWidth(progString);
 306         if (stringWidth > size.height) {
 307             size.height = stringWidth;
 308         }
 309         return size;
 310     }
 311 
 312     public Dimension getMinimumSize(final JComponent c) {
 313         if (isCircular) {
 314             return getCircularPreferredSize();
 315         }
 316 
 317         final Dimension pref = getPreferredSize(progressBar);
 318 
 319         // The Minimum size for this component is 10.
 320         // The rationale here is that there should be at least one pixel per 10 percent.
 321         if (isHorizontal()) {
 322             pref.width = 10;
 323         } else {
 324             pref.height = 10;
 325         }
 326 
 327         return pref;
 328     }
 329 
 330     public Dimension getMaximumSize(final JComponent c) {
 331         if (isCircular) {
 332             return getCircularPreferredSize();
 333         }
 334 
 335         final Dimension pref = getPreferredSize(progressBar);
 336 
 337         if (isHorizontal()) {
 338             pref.width = Short.MAX_VALUE;
 339         } else {
 340             pref.height = Short.MAX_VALUE;
 341         }
 342 
 343         return pref;
 344     }
 345 
 346     public void applySizeFor(final JComponent c, final Size size) {
 347         painter.state.set(sizeVariant = size == Size.MINI ? Size.SMALL : sizeVariant); // CUI doesn't support mini progress bars right now
 348     }
 349 
 350     protected void startAnimationTimer() {
 351         if (animator == null) animator = new Animator();
 352         animator.start();
 353         isAnimating = true;
 354     }
 355 
 356     protected void stopAnimationTimer() {
 357         if (animator != null) animator.stop();
 358         isAnimating = false;
 359     }
 360 
 361     private final Rectangle fUpdateArea = new Rectangle(0, 0, 0, 0);
 362     private final Dimension fLastSize = new Dimension(0, 0);
 363     protected Rectangle getRepaintRect() {
 364         int height = progressBar.getHeight();
 365         int width = progressBar.getWidth();
 366 
 367         if (isCircular) {
 368             return new Rectangle(20, 20);
 369         }
 370 
 371         if (fLastSize.height == height && fLastSize.width == width) {
 372             return fUpdateArea;
 373         }
 374 
 375         int x = 0;
 376         int y = 0;
 377         fLastSize.height = height;
 378         fLastSize.width = width;
 379 
 380         final int maxHeight = getMaxProgressBarHeight();
 381 
 382         if (isHorizontal()) {
 383             final int excessHeight = height - maxHeight;
 384             y += excessHeight / 2;
 385             height = maxHeight;
 386         } else {
 387             final int excessHeight = width - maxHeight;
 388             x += excessHeight / 2;
 389             width = maxHeight;
 390         }
 391 
 392         fUpdateArea.setBounds(x, y, width, height);
 393 
 394         return fUpdateArea;
 395     }
 396 
 397     protected int getMaxProgressBarHeight() {
 398         return getSizeDescriptor().get(sizeVariant).h;
 399     }
 400 
 401     protected boolean isHorizontal() {
 402         return progressBar.getOrientation() == SwingConstants.HORIZONTAL;
 403     }
 404 
 405     protected void revalidateAnimationTimers() {
 406         if (progressBar.isIndeterminate()) return;
 407 
 408         if (!isAnimating) {
 409             startAnimationTimer(); // only starts if supposed to!
 410             return;
 411         }
 412 
 413         final BoundedRangeModel model = progressBar.getModel();
 414         final double currentValue = model.getValue();
 415         if ((currentValue == model.getMaximum()) || (currentValue == model.getMinimum())) {
 416             stopAnimationTimer();
 417         }
 418     }
 419 
 420     protected void repaint() {
 421         final Rectangle repaintRect = getRepaintRect();
 422         if (repaintRect == null) {
 423             progressBar.repaint();
 424             return;
 425         }
 426 
 427         progressBar.repaint(repaintRect);
 428     }
 429 
 430     protected class Animator implements ActionListener {
 431         private static final int MINIMUM_DELAY = 5;
 432         private Timer timer;
 433         private long previousDelay; // used to tune the repaint interval
 434         private long lastCall; // the last time actionPerformed was called
 435         private int repaintInterval;
 436 
 437         public Animator() {
 438             repaintInterval = UIManager.getInt("ProgressBar.repaintInterval");
 439 
 440             // Make sure repaintInterval is reasonable.
 441             if (repaintInterval <= 0) repaintInterval = 100;
 442         }
 443 
 444         protected void start() {
 445             previousDelay = repaintInterval;
 446             lastCall = 0;
 447 
 448             if (timer == null) {
 449                 timer = new Timer(repaintInterval, this);
 450             } else {
 451                 timer.setDelay(repaintInterval);
 452             }
 453 
 454             if (ADJUSTTIMER) {
 455                 timer.setRepeats(false);
 456                 timer.setCoalesce(false);
 457             }
 458 
 459             timer.start();
 460         }
 461 
 462         protected void stop() {
 463             timer.stop();
 464         }
 465 
 466         public void actionPerformed(final ActionEvent e) {
 467             if (!ADJUSTTIMER) {
 468                 repaint();
 469                 return;
 470             }
 471 
 472             final long time = System.currentTimeMillis();
 473 
 474             if (lastCall > 0) {
 475                 // adjust nextDelay
 476                 int nextDelay = (int)(previousDelay - time + lastCall + repaintInterval);
 477                 if (nextDelay < MINIMUM_DELAY) {
 478                     nextDelay = MINIMUM_DELAY;
 479                 }
 480 
 481                 timer.setInitialDelay(nextDelay);
 482                 previousDelay = nextDelay;
 483             }
 484 
 485             timer.start();
 486             lastCall = time;
 487 
 488             repaint();
 489         }
 490     }
 491 }