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 }