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