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.MouseEvent;
30 
31 import javax.swing.*;
32 import javax.swing.event.*;
33 import javax.swing.plaf.ComponentUI;
34 import javax.swing.plaf.basic.BasicSliderUI;
35 
36 import apple.laf.*;
37 import apple.laf.JRSUIUtils.NineSliceMetricsProvider;
38 import apple.laf.JRSUIConstants.*;
39 
40 import com.apple.laf.AquaUtilControlSize.*;
41 import com.apple.laf.AquaImageFactory.NineSliceMetrics;
42 import com.apple.laf.AquaUtils.RecyclableSingleton;
43 
44 public 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
createUI(final JComponent c)94     public static ComponentUI createUI(final JComponent c) {
95         return new AquaSliderUI((JSlider)c);
96     }
97 
AquaSliderUI(final JSlider b)98     public AquaSliderUI(final JSlider b) {
99         super(b);
100     }
101 
installUI(final JComponent c)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 
createTrackListener(final JSlider s)109     protected BasicSliderUI.TrackListener createTrackListener(final JSlider s) {
110         return new TrackListener();
111     }
112 
installListeners(final JSlider s)113     protected void installListeners(final JSlider s) {
114         super.installListeners(s);
115         AquaFocusHandler.install(s);
116         AquaUtilControlSize.addSizePropertyListener(s);
117     }
118 
uninstallListeners(final JSlider s)119     protected void uninstallListeners(final JSlider s) {
120         AquaUtilControlSize.removeSizePropertyListener(s);
121         AquaFocusHandler.uninstall(s);
122         super.uninstallListeners(s);
123     }
124 
applySizeFor(final JComponent c, final Size size)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
paint(final Graphics g, final JComponent c)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
paintTrack(final Graphics g, final JComponent c, final Orientation orientation, final State state)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
paintThumb(final Graphics g, final JComponent c, final Orientation orientation, final State state)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 
getDirection(final Orientation orientation)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 
getState()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 
paintTicks(final Graphics g)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
calculateThumbLocation()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
calculateThumbSize()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 
shouldUseArrowThumb()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 
calculateTickRect()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
getPreferredHorizontalSize()295     public Dimension getPreferredHorizontalSize() {
296         return new Dimension(190, 21);
297     }
298 
getPreferredVerticalSize()299     public Dimension getPreferredVerticalSize() {
300         return new Dimension(21, 190);
301     }
302 
createChangeListener(final JSlider s)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