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