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