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.MouseEvent; 30 31import javax.swing.*; 32import javax.swing.event.*; 33import javax.swing.plaf.ComponentUI; 34import javax.swing.plaf.basic.BasicSliderUI; 35 36import apple.laf.*; 37import apple.laf.JRSUIUtils.NineSliceMetricsProvider; 38import apple.laf.JRSUIConstants.*; 39 40import com.apple.laf.AquaUtilControlSize.*; 41import com.apple.laf.AquaImageFactory.NineSliceMetrics; 42import com.apple.laf.AquaUtils.RecyclableSingleton; 43 44public class AquaSliderUI extends BasicSliderUI implements Sizeable { 45// static final Dimension roundThumbSize = new Dimension(21 + 4, 21 + 4); // +2px on both sides for focus fuzz 46// static final Dimension pointingThumbSize = new Dimension(19 + 4, 22 + 4); 47 48 private static final RecyclableSingleton<SizeDescriptor> roundThumbDescriptor = new RecyclableSingleton<SizeDescriptor>() { 49 protected SizeDescriptor getInstance() { 50 return new SizeDescriptor(new SizeVariant(25, 25)) { 51 public SizeVariant deriveSmall(final SizeVariant v) { 52 return super.deriveSmall(v.alterMinSize(-2, -2)); 53 } 54 public SizeVariant deriveMini(final SizeVariant v) { 55 return super.deriveMini(v.alterMinSize(-2, -2)); 56 } 57 }; 58 } 59 }; 60 private static final RecyclableSingleton<SizeDescriptor> pointingThumbDescriptor = new RecyclableSingleton<SizeDescriptor>() { 61 protected SizeDescriptor getInstance() { 62 return new SizeDescriptor(new SizeVariant(23, 26)) { 63 public SizeVariant deriveSmall(final SizeVariant v) { 64 return super.deriveSmall(v.alterMinSize(-2, -2)); 65 } 66 public SizeVariant deriveMini(final SizeVariant v) { 67 return super.deriveMini(v.alterMinSize(-2, -2)); 68 } 69 }; 70 } 71 }; 72 73 static final AquaPainter<JRSUIState> trackPainter = AquaPainter.create(JRSUIStateFactory.getSliderTrack(), new NineSliceMetricsProvider() { 74 @Override 75 public NineSliceMetrics getNineSliceMetricsForState(JRSUIState state) { 76 if (state.is(Orientation.VERTICAL)) { 77 return new NineSliceMetrics(5, 7, 0, 0, 3, 3, true, false, true); 78 } 79 return new NineSliceMetrics(7, 5, 3, 3, 0, 0, true, true, false); 80 } 81 }); 82 final AquaPainter<JRSUIState> thumbPainter = AquaPainter.create(JRSUIStateFactory.getSliderThumb()); 83 84 protected Color tickColor; 85 protected Color disabledTickColor; 86 87 protected transient boolean fIsDragging = false; 88 89 // From AppearanceManager doc 90 static final int kTickWidth = 3; 91 static final int kTickLength = 8; 92 93 // Create PLAF 94 public static ComponentUI createUI(final JComponent c) { 95 return new AquaSliderUI((JSlider)c); 96 } 97 98 public AquaSliderUI(final JSlider b) { 99 super(b); 100 } 101 102 public void installUI(final JComponent c) { 103 super.installUI(c); 104 105 LookAndFeel.installProperty(slider, "opaque", Boolean.FALSE); 106 tickColor = UIManager.getColor("Slider.tickColor"); 107 } 108 109 protected BasicSliderUI.TrackListener createTrackListener(final JSlider s) { 110 return new TrackListener(); 111 } 112 113 protected void installListeners(final JSlider s) { 114 super.installListeners(s); 115 AquaFocusHandler.install(s); 116 AquaUtilControlSize.addSizePropertyListener(s); 117 } 118 119 protected void uninstallListeners(final JSlider s) { 120 AquaUtilControlSize.removeSizePropertyListener(s); 121 AquaFocusHandler.uninstall(s); 122 super.uninstallListeners(s); 123 } 124 125 public void applySizeFor(final JComponent c, final Size size) { 126 thumbPainter.state.set(size); 127 trackPainter.state.set(size); 128 } 129 130 // Paint Methods 131 public void paint(final Graphics g, final JComponent c) { 132 // We have to override paint of BasicSliderUI because we need slight differences. 133 // We don't paint focus the same way - it is part of the thumb. 134 // We also need to repaint the whole track when the thumb moves. 135 recalculateIfInsetsChanged(); 136 final Rectangle clip = g.getClipBounds(); 137 138 final Orientation orientation = slider.getOrientation() == SwingConstants.HORIZONTAL ? Orientation.HORIZONTAL : Orientation.VERTICAL; 139 final State state = getState(); 140 141 if (slider.getPaintTrack()) { 142 // This is needed for when this is used as a renderer. It is the same as BasicSliderUI.java 143 // and is missing from our reimplementation. 144 // 145 // <rdar://problem/3721898> JSlider in TreeCellRenderer component not painted properly. 146 // 147 final boolean trackIntersectsClip = clip.intersects(trackRect); 148 if (!trackIntersectsClip) { 149 calculateGeometry(); 150 } 151 152 if (trackIntersectsClip || clip.intersects(thumbRect)) paintTrack(g, c, orientation, state); 153 } 154 155 if (slider.getPaintTicks() && clip.intersects(tickRect)) { 156 paintTicks(g); 157 } 158 159 if (slider.getPaintLabels() && clip.intersects(labelRect)) { 160 paintLabels(g); 161 } 162 163 if (clip.intersects(thumbRect)) { 164 paintThumb(g, c, orientation, state); 165 } 166 } 167 168 // Paints track and thumb 169 public void paintTrack(final Graphics g, final JComponent c, final Orientation orientation, final State state) { 170 trackPainter.state.set(orientation); 171 trackPainter.state.set(state); 172 173 // for debugging 174 //g.setColor(Color.green); 175 //g.drawRect(trackRect.x, trackRect.y, trackRect.width - 1, trackRect.height - 1); 176 trackPainter.paint(g, c, trackRect.x, trackRect.y, trackRect.width, trackRect.height); 177 } 178 179 // Paints thumb only 180 public void paintThumb(final Graphics g, final JComponent c, final Orientation orientation, final State state) { 181 thumbPainter.state.set(orientation); 182 thumbPainter.state.set(state); 183 thumbPainter.state.set(slider.hasFocus() ? Focused.YES : Focused.NO); 184 thumbPainter.state.set(getDirection(orientation)); 185 186 // for debugging 187 //g.setColor(Color.blue); 188 //g.drawRect(thumbRect.x, thumbRect.y, thumbRect.width - 1, thumbRect.height - 1); 189 thumbPainter.paint(g, c, thumbRect.x, thumbRect.y, thumbRect.width, thumbRect.height); 190 } 191 192 Direction getDirection(final Orientation orientation) { 193 if (shouldUseArrowThumb()) { 194 return orientation == Orientation.HORIZONTAL ? Direction.DOWN : Direction.RIGHT; 195 } 196 197 return Direction.NONE; 198 } 199 200 State getState() { 201 if (!slider.isEnabled()) { 202 return State.DISABLED; 203 } 204 205 if (fIsDragging) { 206 return State.PRESSED; 207 } 208 209 if (!AquaFocusHandler.isActive(slider)) { 210 return State.INACTIVE; 211 } 212 213 return State.ACTIVE; 214 } 215 216 public void paintTicks(final Graphics g) { 217 if (slider.isEnabled()) { 218 g.setColor(tickColor); 219 } else { 220 if (disabledTickColor == null) { 221 disabledTickColor = new Color(tickColor.getRed(), tickColor.getGreen(), tickColor.getBlue(), tickColor.getAlpha() / 2); 222 } 223 g.setColor(disabledTickColor); 224 } 225 226 super.paintTicks(g); 227 } 228 229 // Layout Methods 230 231 // Used lots 232 protected void calculateThumbLocation() { 233 super.calculateThumbLocation(); 234 235 if (shouldUseArrowThumb()) { 236 final boolean isHorizonatal = slider.getOrientation() == SwingConstants.HORIZONTAL; 237 final Size size = AquaUtilControlSize.getUserSizeFrom(slider); 238 239 if (size == Size.REGULAR) { 240 if (isHorizonatal) thumbRect.y += 3; else thumbRect.x += 2; return; 241 } 242 243 if (size == Size.SMALL) { 244 if (isHorizonatal) thumbRect.y += 2; else thumbRect.x += 2; return; 245 } 246 247 if (size == Size.MINI) { 248 if (isHorizonatal) thumbRect.y += 1; return; 249 } 250 } 251 } 252 253 // Only called from calculateGeometry 254 protected void calculateThumbSize() { 255 final SizeDescriptor descriptor = shouldUseArrowThumb() ? pointingThumbDescriptor.get() : roundThumbDescriptor.get(); 256 final SizeVariant variant = descriptor.get(slider); 257 258 if (slider.getOrientation() == SwingConstants.HORIZONTAL) { 259 thumbRect.setSize(variant.w, variant.h); 260 } else { 261 thumbRect.setSize(variant.h, variant.w); 262 } 263 } 264 265 protected boolean shouldUseArrowThumb() { 266 if (slider.getPaintTicks() || slider.getPaintLabels()) return true; 267 268 final Object shouldPaintArrowThumbProperty = slider.getClientProperty("Slider.paintThumbArrowShape"); 269 if (shouldPaintArrowThumbProperty != null && shouldPaintArrowThumbProperty instanceof Boolean) { 270 return ((Boolean)shouldPaintArrowThumbProperty).booleanValue(); 271 } 272 273 return false; 274 } 275 276 protected void calculateTickRect() { 277 // super assumes tickRect ends align with trackRect ends. 278 // Ours need to inset by trackBuffer 279 // Ours also needs to be *inside* trackRect 280 final int tickLength = slider.getPaintTicks() ? getTickLength() : 0; 281 if (slider.getOrientation() == SwingConstants.HORIZONTAL) { 282 tickRect.height = tickLength; 283 tickRect.x = trackRect.x + trackBuffer; 284 tickRect.y = trackRect.y + trackRect.height - (tickRect.height / 2); 285 tickRect.width = trackRect.width - (trackBuffer * 2); 286 } else { 287 tickRect.width = tickLength; 288 tickRect.x = trackRect.x + trackRect.width - (tickRect.width / 2); 289 tickRect.y = trackRect.y + trackBuffer; 290 tickRect.height = trackRect.height - (trackBuffer * 2); 291 } 292 } 293 294 // Basic's preferred size doesn't allow for our focus ring, throwing off things like SwingSet2 295 public Dimension getPreferredHorizontalSize() { 296 return new Dimension(190, 21); 297 } 298 299 public Dimension getPreferredVerticalSize() { 300 return new Dimension(21, 190); 301 } 302 303 protected ChangeListener createChangeListener(final JSlider s) { 304 return new ChangeListener() { 305 public void stateChanged(final ChangeEvent e) { 306 if (fIsDragging) return; 307 calculateThumbLocation(); 308 slider.repaint(); 309 } 310 }; 311 } 312 313 // This is copied almost verbatim from superclass, except we changed things to use fIsDragging 314 // instead of isDragging since isDragging was a private member. 315 class TrackListener extends javax.swing.plaf.basic.BasicSliderUI.TrackListener { 316 protected transient int offset; 317 protected transient int currentMouseX = -1, currentMouseY = -1; 318 319 public void mouseReleased(final MouseEvent e) { 320 if (!slider.isEnabled()) return; 321 322 currentMouseX = -1; 323 currentMouseY = -1; 324 325 offset = 0; 326 scrollTimer.stop(); 327 328 // This is the way we have to determine snap-to-ticks. It's hard to explain 329 // but since ChangeEvents don't give us any idea what has changed we don't 330 // have a way to stop the thumb bounds from being recalculated. Recalculating 331 // the thumb bounds moves the thumb over the current value (i.e., snapping 332 // to the ticks). 333 if (slider.getSnapToTicks() /*|| slider.getSnapToValue()*/) { 334 fIsDragging = false; 335 slider.setValueIsAdjusting(false); 336 } else { 337 slider.setValueIsAdjusting(false); 338 fIsDragging = false; 339 } 340 341 slider.repaint(); 342 } 343 344 public void mousePressed(final MouseEvent e) { 345 if (!slider.isEnabled()) return; 346 347 // We should recalculate geometry just before 348 // calculation of the thumb movement direction. 349 // It is important for the case, when JSlider 350 // is a cell editor in JTable. See 6348946. 351 calculateGeometry(); 352 353 final boolean firstClick = (currentMouseX == -1) && (currentMouseY == -1); 354 355 currentMouseX = e.getX(); 356 currentMouseY = e.getY(); 357 358 if (slider.isRequestFocusEnabled()) { 359 slider.requestFocus(); 360 } 361 362 boolean isMouseEventInThumb = thumbRect.contains(currentMouseX, currentMouseY); 363 364 // we don't want to move the thumb if we just clicked on the edge of the thumb 365 if (!firstClick || !isMouseEventInThumb) { 366 slider.setValueIsAdjusting(true); 367 368 switch (slider.getOrientation()) { 369 case SwingConstants.VERTICAL: 370 slider.setValue(valueForYPosition(currentMouseY)); 371 break; 372 case SwingConstants.HORIZONTAL: 373 slider.setValue(valueForXPosition(currentMouseX)); 374 break; 375 } 376 377 slider.setValueIsAdjusting(false); 378 379 isMouseEventInThumb = true; // since we just moved it in there 380 } 381 382 // Clicked in the Thumb area? 383 if (isMouseEventInThumb) { 384 switch (slider.getOrientation()) { 385 case SwingConstants.VERTICAL: 386 offset = currentMouseY - thumbRect.y; 387 break; 388 case SwingConstants.HORIZONTAL: 389 offset = currentMouseX - thumbRect.x; 390 break; 391 } 392 393 fIsDragging = true; 394 return; 395 } 396 397 fIsDragging = false; 398 } 399 400 public boolean shouldScroll(final int direction) { 401 final Rectangle r = thumbRect; 402 if (slider.getOrientation() == SwingConstants.VERTICAL) { 403 if (drawInverted() ? direction < 0 : direction > 0) { 404 if (r.y + r.height <= currentMouseY) return false; 405 } else { 406 if (r.y >= currentMouseY) return false; 407 } 408 } else { 409 if (drawInverted() ? direction < 0 : direction > 0) { 410 if (r.x + r.width >= currentMouseX) return false; 411 } else { 412 if (r.x <= currentMouseX) return false; 413 } 414 } 415 416 if (direction > 0 && slider.getValue() + slider.getExtent() >= slider.getMaximum()) { 417 return false; 418 } 419 420 if (direction < 0 && slider.getValue() <= slider.getMinimum()) { 421 return false; 422 } 423 424 return true; 425 } 426 427 /** 428 * Set the models value to the position of the top/left 429 * of the thumb relative to the origin of the track. 430 */ 431 public void mouseDragged(final MouseEvent e) { 432 int thumbMiddle = 0; 433 434 if (!slider.isEnabled()) return; 435 436 currentMouseX = e.getX(); 437 currentMouseY = e.getY(); 438 439 if (!fIsDragging) return; 440 441 slider.setValueIsAdjusting(true); 442 443 switch (slider.getOrientation()) { 444 case SwingConstants.VERTICAL: 445 final int halfThumbHeight = thumbRect.height / 2; 446 int thumbTop = e.getY() - offset; 447 int trackTop = trackRect.y; 448 int trackBottom = trackRect.y + (trackRect.height - 1); 449 final int vMax = yPositionForValue(slider.getMaximum() - slider.getExtent()); 450 451 if (drawInverted()) { 452 trackBottom = vMax; 453 } else { 454 trackTop = vMax; 455 } 456 thumbTop = Math.max(thumbTop, trackTop - halfThumbHeight); 457 thumbTop = Math.min(thumbTop, trackBottom - halfThumbHeight); 458 459 setThumbLocation(thumbRect.x, thumbTop); 460 461 thumbMiddle = thumbTop + halfThumbHeight; 462 slider.setValue(valueForYPosition(thumbMiddle)); 463 break; 464 case SwingConstants.HORIZONTAL: 465 final int halfThumbWidth = thumbRect.width / 2; 466 int thumbLeft = e.getX() - offset; 467 int trackLeft = trackRect.x; 468 int trackRight = trackRect.x + (trackRect.width - 1); 469 final int hMax = xPositionForValue(slider.getMaximum() - slider.getExtent()); 470 471 if (drawInverted()) { 472 trackLeft = hMax; 473 } else { 474 trackRight = hMax; 475 } 476 thumbLeft = Math.max(thumbLeft, trackLeft - halfThumbWidth); 477 thumbLeft = Math.min(thumbLeft, trackRight - halfThumbWidth); 478 479 setThumbLocation(thumbLeft, thumbRect.y); 480 481 thumbMiddle = thumbLeft + halfThumbWidth; 482 slider.setValue(valueForXPosition(thumbMiddle)); 483 break; 484 default: 485 return; 486 } 487 488 // enable live snap-to-ticks <rdar://problem/3165310> 489 if (slider.getSnapToTicks()) { 490 calculateThumbLocation(); 491 setThumbLocation(thumbRect.x, thumbRect.y); // need to call to refresh the repaint region 492 } 493 } 494 495 public void mouseMoved(final MouseEvent e) { } 496 } 497 498 // Super handles snap-to-ticks by recalculating the thumb rect in the TrackListener 499 // See setThumbLocation for why that doesn't work 500 int getScale() { 501 if (!slider.getSnapToTicks()) return 1; 502 int scale = slider.getMinorTickSpacing(); 503 if (scale < 1) scale = slider.getMajorTickSpacing(); 504 if (scale < 1) return 1; 505 return scale; 506 } 507} 508