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