/* * Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.apple.laf; import java.awt.*; import java.awt.event.MouseEvent; import javax.swing.*; import javax.swing.event.*; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicSliderUI; import apple.laf.*; import apple.laf.JRSUIUtils.NineSliceMetricsProvider; import apple.laf.JRSUIConstants.*; import com.apple.laf.AquaUtilControlSize.*; import com.apple.laf.AquaImageFactory.NineSliceMetrics; import com.apple.laf.AquaUtils.RecyclableSingleton; public class AquaSliderUI extends BasicSliderUI implements Sizeable { // static final Dimension roundThumbSize = new Dimension(21 + 4, 21 + 4); // +2px on both sides for focus fuzz // static final Dimension pointingThumbSize = new Dimension(19 + 4, 22 + 4); protected static final RecyclableSingleton<SizeDescriptor> roundThumbDescriptor = new RecyclableSingleton<SizeDescriptor>() { protected SizeDescriptor getInstance() { return new SizeDescriptor(new SizeVariant(25, 25)) { public SizeVariant deriveSmall(final SizeVariant v) { return super.deriveSmall(v.alterMinSize(-2, -2)); } public SizeVariant deriveMini(final SizeVariant v) { return super.deriveMini(v.alterMinSize(-2, -2)); } }; } }; protected static final RecyclableSingleton<SizeDescriptor> pointingThumbDescriptor = new RecyclableSingleton<SizeDescriptor>() { protected SizeDescriptor getInstance() { return new SizeDescriptor(new SizeVariant(23, 26)) { public SizeVariant deriveSmall(final SizeVariant v) { return super.deriveSmall(v.alterMinSize(-2, -2)); } public SizeVariant deriveMini(final SizeVariant v) { return super.deriveMini(v.alterMinSize(-2, -2)); } }; } }; static final AquaPainter<JRSUIState> trackPainter = AquaPainter.create(JRSUIStateFactory.getSliderTrack(), new NineSliceMetricsProvider() { @Override public NineSliceMetrics getNineSliceMetricsForState(JRSUIState state) { if (state.is(Orientation.VERTICAL)) { return new NineSliceMetrics(5, 7, 0, 0, 3, 3, true, false, true); } return new NineSliceMetrics(7, 5, 3, 3, 0, 0, true, true, false); } }); final AquaPainter<JRSUIState> thumbPainter = AquaPainter.create(JRSUIStateFactory.getSliderThumb()); protected Color tickColor; protected Color disabledTickColor; protected transient boolean fIsDragging = false; // From AppearanceManager doc static final int kTickWidth = 3; static final int kTickLength = 8; // Create PLAF public static ComponentUI createUI(final JComponent c) { return new AquaSliderUI((JSlider)c); } public AquaSliderUI(final JSlider b) { super(b); } public void installUI(final JComponent c) { super.installUI(c); LookAndFeel.installProperty(slider, "opaque", Boolean.FALSE); tickColor = UIManager.getColor("Slider.tickColor"); } protected BasicSliderUI.TrackListener createTrackListener(final JSlider s) { return new TrackListener(); } protected void installListeners(final JSlider s) { super.installListeners(s); AquaFocusHandler.install(s); AquaUtilControlSize.addSizePropertyListener(s); } protected void uninstallListeners(final JSlider s) { AquaUtilControlSize.removeSizePropertyListener(s); AquaFocusHandler.uninstall(s); super.uninstallListeners(s); } public void applySizeFor(final JComponent c, final Size size) { thumbPainter.state.set(size); trackPainter.state.set(size); } // Paint Methods public void paint(final Graphics g, final JComponent c) { // We have to override paint of BasicSliderUI because we need slight differences. // We don't paint focus the same way - it is part of the thumb. // We also need to repaint the whole track when the thumb moves. recalculateIfInsetsChanged(); final Rectangle clip = g.getClipBounds(); final Orientation orientation = slider.getOrientation() == SwingConstants.HORIZONTAL ? Orientation.HORIZONTAL : Orientation.VERTICAL; final State state = getState(); if (slider.getPaintTrack()) { // This is needed for when this is used as a renderer. It is the same as BasicSliderUI.java // and is missing from our reimplementation. // // <rdar://problem/3721898> JSlider in TreeCellRenderer component not painted properly. // final boolean trackIntersectsClip = clip.intersects(trackRect); if (!trackIntersectsClip) { calculateGeometry(); } if (trackIntersectsClip || clip.intersects(thumbRect)) paintTrack(g, c, orientation, state); } if (slider.getPaintTicks() && clip.intersects(tickRect)) { paintTicks(g); } if (slider.getPaintLabels() && clip.intersects(labelRect)) { paintLabels(g); } if (clip.intersects(thumbRect)) { paintThumb(g, c, orientation, state); } } // Paints track and thumb public void paintTrack(final Graphics g, final JComponent c, final Orientation orientation, final State state) { trackPainter.state.set(orientation); trackPainter.state.set(state); // for debugging //g.setColor(Color.green); //g.drawRect(trackRect.x, trackRect.y, trackRect.width - 1, trackRect.height - 1); trackPainter.paint(g, c, trackRect.x, trackRect.y, trackRect.width, trackRect.height); } // Paints thumb only public void paintThumb(final Graphics g, final JComponent c, final Orientation orientation, final State state) { thumbPainter.state.set(orientation); thumbPainter.state.set(state); thumbPainter.state.set(slider.hasFocus() ? Focused.YES : Focused.NO); thumbPainter.state.set(getDirection(orientation)); // for debugging //g.setColor(Color.blue); //g.drawRect(thumbRect.x, thumbRect.y, thumbRect.width - 1, thumbRect.height - 1); thumbPainter.paint(g, c, thumbRect.x, thumbRect.y, thumbRect.width, thumbRect.height); } Direction getDirection(final Orientation orientation) { if (shouldUseArrowThumb()) { return orientation == Orientation.HORIZONTAL ? Direction.DOWN : Direction.RIGHT; } return Direction.NONE; } State getState() { if (!slider.isEnabled()) { return State.DISABLED; } if (fIsDragging) { return State.PRESSED; } if (!AquaFocusHandler.isActive(slider)) { return State.INACTIVE; } return State.ACTIVE; } public void paintTicks(final Graphics g) { if (slider.isEnabled()) { g.setColor(tickColor); } else { if (disabledTickColor == null) { disabledTickColor = new Color(tickColor.getRed(), tickColor.getGreen(), tickColor.getBlue(), tickColor.getAlpha() / 2); } g.setColor(disabledTickColor); } super.paintTicks(g); } // Layout Methods // Used lots protected void calculateThumbLocation() { super.calculateThumbLocation(); if (shouldUseArrowThumb()) { final boolean isHorizonatal = slider.getOrientation() == SwingConstants.HORIZONTAL; final Size size = AquaUtilControlSize.getUserSizeFrom(slider); if (size == Size.REGULAR) { if (isHorizonatal) thumbRect.y += 3; else thumbRect.x += 2; return; } if (size == Size.SMALL) { if (isHorizonatal) thumbRect.y += 2; else thumbRect.x += 2; return; } if (size == Size.MINI) { if (isHorizonatal) thumbRect.y += 1; return; } } } // Only called from calculateGeometry protected void calculateThumbSize() { final SizeDescriptor descriptor = shouldUseArrowThumb() ? pointingThumbDescriptor.get() : roundThumbDescriptor.get(); final SizeVariant variant = descriptor.get(slider); if (slider.getOrientation() == SwingConstants.HORIZONTAL) { thumbRect.setSize(variant.w, variant.h); } else { thumbRect.setSize(variant.h, variant.w); } } protected boolean shouldUseArrowThumb() { if (slider.getPaintTicks() || slider.getPaintLabels()) return true; final Object shouldPaintArrowThumbProperty = slider.getClientProperty("Slider.paintThumbArrowShape"); if (shouldPaintArrowThumbProperty != null && shouldPaintArrowThumbProperty instanceof Boolean) { return ((Boolean)shouldPaintArrowThumbProperty).booleanValue(); } return false; } protected void calculateTickRect() { // super assumes tickRect ends align with trackRect ends. // Ours need to inset by trackBuffer // Ours also needs to be *inside* trackRect final int tickLength = slider.getPaintTicks() ? getTickLength() : 0; if (slider.getOrientation() == SwingConstants.HORIZONTAL) { tickRect.height = tickLength; tickRect.x = trackRect.x + trackBuffer; tickRect.y = trackRect.y + trackRect.height - (tickRect.height / 2); tickRect.width = trackRect.width - (trackBuffer * 2); } else { tickRect.width = tickLength; tickRect.x = trackRect.x + trackRect.width - (tickRect.width / 2); tickRect.y = trackRect.y + trackBuffer; tickRect.height = trackRect.height - (trackBuffer * 2); } } // Basic's preferred size doesn't allow for our focus ring, throwing off things like SwingSet2 public Dimension getPreferredHorizontalSize() { return new Dimension(190, 21); } public Dimension getPreferredVerticalSize() { return new Dimension(21, 190); } protected ChangeListener createChangeListener(final JSlider s) { return new ChangeListener() { public void stateChanged(final ChangeEvent e) { if (fIsDragging) return; calculateThumbLocation(); slider.repaint(); } }; } // This is copied almost verbatim from superclass, except we changed things to use fIsDragging // instead of isDragging since isDragging was a private member. class TrackListener extends javax.swing.plaf.basic.BasicSliderUI.TrackListener { protected transient int offset; protected transient int currentMouseX = -1, currentMouseY = -1; public void mouseReleased(final MouseEvent e) { if (!slider.isEnabled()) return; currentMouseX = -1; currentMouseY = -1; offset = 0; scrollTimer.stop(); // This is the way we have to determine snap-to-ticks. It's hard to explain // but since ChangeEvents don't give us any idea what has changed we don't // have a way to stop the thumb bounds from being recalculated. Recalculating // the thumb bounds moves the thumb over the current value (i.e., snapping // to the ticks). if (slider.getSnapToTicks() /*|| slider.getSnapToValue()*/) { fIsDragging = false; slider.setValueIsAdjusting(false); } else { slider.setValueIsAdjusting(false); fIsDragging = false; } slider.repaint(); } public void mousePressed(final MouseEvent e) { if (!slider.isEnabled()) return; // We should recalculate geometry just before // calculation of the thumb movement direction. // It is important for the case, when JSlider // is a cell editor in JTable. See 6348946. calculateGeometry(); final boolean firstClick = (currentMouseX == -1) && (currentMouseY == -1); currentMouseX = e.getX(); currentMouseY = e.getY(); if (slider.isRequestFocusEnabled()) { slider.requestFocus(); } boolean isMouseEventInThumb = thumbRect.contains(currentMouseX, currentMouseY); // we don't want to move the thumb if we just clicked on the edge of the thumb if (!firstClick || !isMouseEventInThumb) { slider.setValueIsAdjusting(true); switch (slider.getOrientation()) { case SwingConstants.VERTICAL: slider.setValue(valueForYPosition(currentMouseY)); break; case SwingConstants.HORIZONTAL: slider.setValue(valueForXPosition(currentMouseX)); break; } slider.setValueIsAdjusting(false); isMouseEventInThumb = true; // since we just moved it in there } // Clicked in the Thumb area? if (isMouseEventInThumb) { switch (slider.getOrientation()) { case SwingConstants.VERTICAL: offset = currentMouseY - thumbRect.y; break; case SwingConstants.HORIZONTAL: offset = currentMouseX - thumbRect.x; break; } fIsDragging = true; return; } fIsDragging = false; } public boolean shouldScroll(final int direction) { final Rectangle r = thumbRect; if (slider.getOrientation() == SwingConstants.VERTICAL) { if (drawInverted() ? direction < 0 : direction > 0) { if (r.y + r.height <= currentMouseY) return false; } else { if (r.y >= currentMouseY) return false; } } else { if (drawInverted() ? direction < 0 : direction > 0) { if (r.x + r.width >= currentMouseX) return false; } else { if (r.x <= currentMouseX) return false; } } if (direction > 0 && slider.getValue() + slider.getExtent() >= slider.getMaximum()) { return false; } if (direction < 0 && slider.getValue() <= slider.getMinimum()) { return false; } return true; } /** * Set the models value to the position of the top/left * of the thumb relative to the origin of the track. */ public void mouseDragged(final MouseEvent e) { int thumbMiddle = 0; if (!slider.isEnabled()) return; currentMouseX = e.getX(); currentMouseY = e.getY(); if (!fIsDragging) return; slider.setValueIsAdjusting(true); switch (slider.getOrientation()) { case SwingConstants.VERTICAL: final int halfThumbHeight = thumbRect.height / 2; int thumbTop = e.getY() - offset; int trackTop = trackRect.y; int trackBottom = trackRect.y + (trackRect.height - 1); final int vMax = yPositionForValue(slider.getMaximum() - slider.getExtent()); if (drawInverted()) { trackBottom = vMax; } else { trackTop = vMax; } thumbTop = Math.max(thumbTop, trackTop - halfThumbHeight); thumbTop = Math.min(thumbTop, trackBottom - halfThumbHeight); setThumbLocation(thumbRect.x, thumbTop); thumbMiddle = thumbTop + halfThumbHeight; slider.setValue(valueForYPosition(thumbMiddle)); break; case SwingConstants.HORIZONTAL: final int halfThumbWidth = thumbRect.width / 2; int thumbLeft = e.getX() - offset; int trackLeft = trackRect.x; int trackRight = trackRect.x + (trackRect.width - 1); final int hMax = xPositionForValue(slider.getMaximum() - slider.getExtent()); if (drawInverted()) { trackLeft = hMax; } else { trackRight = hMax; } thumbLeft = Math.max(thumbLeft, trackLeft - halfThumbWidth); thumbLeft = Math.min(thumbLeft, trackRight - halfThumbWidth); setThumbLocation(thumbLeft, thumbRect.y); thumbMiddle = thumbLeft + halfThumbWidth; slider.setValue(valueForXPosition(thumbMiddle)); break; default: return; } // enable live snap-to-ticks <rdar://problem/3165310> if (slider.getSnapToTicks()) { calculateThumbLocation(); setThumbLocation(thumbRect.x, thumbRect.y); // need to call to refresh the repaint region } } public void mouseMoved(final MouseEvent e) { } } // Super handles snap-to-ticks by recalculating the thumb rect in the TrackListener // See setThumbLocation for why that doesn't work int getScale() { if (!slider.getSnapToTicks()) return 1; int scale = slider.getMinorTickSpacing(); if (scale < 1) scale = slider.getMajorTickSpacing(); if (scale < 1) return 1; return scale; } }