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 }