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.*;
30import java.beans.*;
31import java.util.*;
32
33import javax.swing.*;
34import javax.swing.Timer;
35import javax.swing.event.*;
36import javax.swing.plaf.*;
37
38import apple.laf.*;
39import apple.laf.JRSUIConstants.*;
40import apple.laf.JRSUIState.ScrollBarState;
41
42import com.apple.laf.AquaUtils.RecyclableSingleton;
43
44public class AquaScrollBarUI extends ScrollBarUI {
45    private static final int kInitialDelay = 300;
46    private static final int kNormalDelay = 100;
47
48    // when we make small and mini scrollbars, this will no longer be a constant
49    static final int MIN_ARROW_COLLAPSE_SIZE = 64;
50
51    // tracking state
52    protected boolean fIsDragging;
53    protected Timer fScrollTimer;
54    protected ScrollListener fScrollListener;
55    protected TrackListener fTrackListener;
56    protected Hit fTrackHighlight = Hit.NONE;
57    protected Hit fMousePart = Hit.NONE; // Which arrow (if any) we moused pressed down in (used by arrow drag tracking)
58
59    protected JScrollBar fScrollBar;
60    protected ModelListener fModelListener;
61    protected PropertyChangeListener fPropertyChangeListener;
62
63    protected final AquaPainter<ScrollBarState> painter = AquaPainter.create(JRSUIStateFactory.getScrollBar());
64
65    // Create PLAF
66    public static ComponentUI createUI(final JComponent c) {
67        return new AquaScrollBarUI();
68    }
69
70    public AquaScrollBarUI() { }
71
72    public void installUI(final JComponent c) {
73        fScrollBar = (JScrollBar)c;
74        installListeners();
75        configureScrollBarColors();
76    }
77
78    public void uninstallUI(final JComponent c) {
79        uninstallListeners();
80        fScrollBar = null;
81    }
82
83    protected void configureScrollBarColors() {
84        LookAndFeel.installColors(fScrollBar, "ScrollBar.background", "ScrollBar.foreground");
85    }
86
87    protected TrackListener createTrackListener() {
88        return new TrackListener();
89    }
90
91    protected ScrollListener createScrollListener() {
92        return new ScrollListener();
93    }
94
95    protected void installListeners() {
96        fTrackListener = createTrackListener();
97        fModelListener = createModelListener();
98        fPropertyChangeListener = createPropertyChangeListener();
99        fScrollBar.addMouseListener(fTrackListener);
100        fScrollBar.addMouseMotionListener(fTrackListener);
101        fScrollBar.getModel().addChangeListener(fModelListener);
102        fScrollBar.addPropertyChangeListener(fPropertyChangeListener);
103        fScrollListener = createScrollListener();
104        fScrollTimer = new Timer(kNormalDelay, fScrollListener);
105        fScrollTimer.setInitialDelay(kInitialDelay); // default InitialDelay?
106    }
107
108    protected void uninstallListeners() {
109        fScrollTimer.stop();
110        fScrollTimer = null;
111        fScrollBar.getModel().removeChangeListener(fModelListener);
112        fScrollBar.removeMouseListener(fTrackListener);
113        fScrollBar.removeMouseMotionListener(fTrackListener);
114        fScrollBar.removePropertyChangeListener(fPropertyChangeListener);
115    }
116
117    protected PropertyChangeListener createPropertyChangeListener() {
118        return new PropertyChangeHandler();
119    }
120
121    protected ModelListener createModelListener() {
122        return new ModelListener();
123    }
124
125    protected void syncState(final JComponent c) {
126        final ScrollBarState scrollBarState = painter.state;
127        scrollBarState.set(isHorizontal() ? Orientation.HORIZONTAL : Orientation.VERTICAL);
128
129        final float trackExtent = fScrollBar.getMaximum() - fScrollBar.getMinimum() - fScrollBar.getModel().getExtent();
130        if (trackExtent <= 0.0f) {
131            scrollBarState.set(NothingToScroll.YES);
132            return;
133        }
134
135        final ScrollBarPart pressedPart = getPressedPart();
136        scrollBarState.set(pressedPart);
137        scrollBarState.set(getState(c, pressedPart));
138        scrollBarState.set(NothingToScroll.NO);
139        scrollBarState.setValue((fScrollBar.getValue() - fScrollBar.getMinimum()) / trackExtent);
140        scrollBarState.setThumbStart(getThumbStart());
141        scrollBarState.setThumbPercent(getThumbPercent());
142        scrollBarState.set(shouldShowArrows() ? ShowArrows.YES : ShowArrows.NO);
143    }
144
145    public void paint(final Graphics g, final JComponent c) {
146        syncState(c);
147        painter.paint(g, c, 0, 0, fScrollBar.getWidth(), fScrollBar.getHeight());
148    }
149
150    protected State getState(final JComponent c, final ScrollBarPart pressedPart) {
151        if (!AquaFocusHandler.isActive(c)) return State.INACTIVE;
152        if (!c.isEnabled()) return State.INACTIVE;
153        if (pressedPart != ScrollBarPart.NONE) return State.PRESSED;
154        return State.ACTIVE;
155    }
156
157    private static final RecyclableSingleton<Map<Hit, ScrollBarPart>> hitToPressedPartMap = new RecyclableSingleton<Map<Hit,ScrollBarPart>>(){
158        @Override
159        protected Map<Hit, ScrollBarPart> getInstance() {
160            final Map<Hit, ScrollBarPart> map = new HashMap<Hit, ScrollBarPart>(7);
161            map.put(ScrollBarHit.ARROW_MAX, ScrollBarPart.ARROW_MAX);
162            map.put(ScrollBarHit.ARROW_MIN, ScrollBarPart.ARROW_MIN);
163            map.put(ScrollBarHit.ARROW_MAX_INSIDE, ScrollBarPart.ARROW_MAX_INSIDE);
164            map.put(ScrollBarHit.ARROW_MIN_INSIDE, ScrollBarPart.ARROW_MIN_INSIDE);
165            map.put(ScrollBarHit.TRACK_MAX, ScrollBarPart.TRACK_MAX);
166            map.put(ScrollBarHit.TRACK_MIN, ScrollBarPart.TRACK_MIN);
167            map.put(ScrollBarHit.THUMB, ScrollBarPart.THUMB);
168            return map;
169        }
170    };
171    protected ScrollBarPart getPressedPart() {
172        if (!fTrackListener.fInArrows || !fTrackListener.fStillInArrow) return ScrollBarPart.NONE;
173        final ScrollBarPart pressedPart = hitToPressedPartMap.get().get(fMousePart);
174        if (pressedPart == null) return ScrollBarPart.NONE;
175        return pressedPart;
176    }
177
178    protected boolean shouldShowArrows() {
179        return MIN_ARROW_COLLAPSE_SIZE < (isHorizontal() ? fScrollBar.getWidth() : fScrollBar.getHeight());
180    }
181
182    // Layout Methods
183    // Layout is controlled by the user in the Appearance Control Panel
184    // Theme will redraw correctly for the current layout
185    public void layoutContainer(final Container fScrollBarContainer) {
186        fScrollBar.repaint();
187        fScrollBar.revalidate();
188    }
189
190    protected Rectangle getTrackBounds() {
191        return new Rectangle(0, 0, fScrollBar.getWidth(), fScrollBar.getHeight());
192    }
193
194    protected Rectangle getDragBounds() {
195        return new Rectangle(0, 0, fScrollBar.getWidth(), fScrollBar.getHeight());
196    }
197
198    protected void startTimer(final boolean initial) {
199        fScrollTimer.setInitialDelay(initial ? kInitialDelay : kNormalDelay); // default InitialDelay?
200        fScrollTimer.start();
201    }
202
203    protected void scrollByBlock(final int direction) {
204        synchronized(fScrollBar) {
205            final int oldValue = fScrollBar.getValue();
206            final int blockIncrement = fScrollBar.getBlockIncrement(direction);
207            final int delta = blockIncrement * ((direction > 0) ? +1 : -1);
208
209            fScrollBar.setValue(oldValue + delta);
210            fTrackHighlight = direction > 0 ? ScrollBarHit.TRACK_MAX : ScrollBarHit.TRACK_MIN;
211            fScrollBar.repaint();
212            fScrollListener.setDirection(direction);
213            fScrollListener.setScrollByBlock(true);
214        }
215    }
216
217    protected void scrollByUnit(final int direction) {
218        synchronized(fScrollBar) {
219            int delta = fScrollBar.getUnitIncrement(direction);
220            if (direction <= 0) delta = -delta;
221
222            fScrollBar.setValue(delta + fScrollBar.getValue());
223            fScrollBar.repaint();
224            fScrollListener.setDirection(direction);
225            fScrollListener.setScrollByBlock(false);
226        }
227    }
228
229    protected Hit getPartHit(final int x, final int y) {
230        syncState(fScrollBar);
231        return JRSUIUtils.HitDetection.getHitForPoint(painter.getControl(), 0, 0, fScrollBar.getWidth(), fScrollBar.getHeight(), x, y);
232    }
233
234    protected class PropertyChangeHandler implements PropertyChangeListener {
235        public void propertyChange(final PropertyChangeEvent e) {
236            final String propertyName = e.getPropertyName();
237
238            if ("model".equals(propertyName)) {
239                final BoundedRangeModel oldModel = (BoundedRangeModel)e.getOldValue();
240                final BoundedRangeModel newModel = (BoundedRangeModel)e.getNewValue();
241                oldModel.removeChangeListener(fModelListener);
242                newModel.addChangeListener(fModelListener);
243                fScrollBar.repaint();
244                fScrollBar.revalidate();
245            } else if (AquaFocusHandler.FRAME_ACTIVE_PROPERTY.equals(propertyName)) {
246                fScrollBar.repaint();
247            }
248        }
249    }
250
251    protected class ModelListener implements ChangeListener {
252        public void stateChanged(final ChangeEvent e) {
253            layoutContainer(fScrollBar);
254        }
255    }
256
257    // Track mouse drags.
258    protected class TrackListener extends MouseAdapter implements MouseMotionListener {
259        protected transient int fCurrentMouseX, fCurrentMouseY;
260        protected transient boolean fInArrows; // are we currently tracking arrows?
261        protected transient boolean fStillInArrow = false; // Whether mouse is in an arrow during arrow tracking
262        protected transient boolean fStillInTrack = false; // Whether mouse is in the track during pageup/down tracking
263        protected transient int fFirstMouseX, fFirstMouseY, fFirstValue; // Values for getValueFromOffset
264
265        public void mouseReleased(final MouseEvent e) {
266            if (!fScrollBar.isEnabled()) return;
267            if (fInArrows) {
268                mouseReleasedInArrows(e);
269            } else {
270                mouseReleasedInTrack(e);
271            }
272
273            fInArrows = false;
274            fStillInArrow = false;
275            fStillInTrack = false;
276
277            fScrollBar.repaint();
278            fScrollBar.revalidate();
279        }
280
281        public void mousePressed(final MouseEvent e) {
282            if (!fScrollBar.isEnabled()) return;
283
284            final Hit part = getPartHit(e.getX(), e.getY());
285            fInArrows = HitUtil.isArrow(part);
286            if (fInArrows) {
287                mousePressedInArrows(e, part);
288            } else {
289                if (part == Hit.NONE) {
290                    fTrackHighlight = Hit.NONE;
291                } else {
292                    mousePressedInTrack(e, part);
293                }
294            }
295        }
296
297        public void mouseDragged(final MouseEvent e) {
298            if (!fScrollBar.isEnabled()) return;
299
300            if (fInArrows) {
301                mouseDraggedInArrows(e);
302            } else if (fIsDragging) {
303                mouseDraggedInTrack(e);
304            } else {
305                // In pageup/down zones
306
307                // check that thumb has not been scrolled under the mouse cursor
308                final Hit previousPart = getPartHit(fCurrentMouseX, fCurrentMouseY);
309                if (!HitUtil.isTrack(previousPart)) {
310                    fStillInTrack = false;
311                }
312
313                fCurrentMouseX = e.getX();
314                fCurrentMouseY = e.getY();
315
316                final Hit part = getPartHit(e.getX(), e.getY());
317                final boolean temp = HitUtil.isTrack(part);
318                if (temp == fStillInTrack) return;
319
320                fStillInTrack = temp;
321                if (!fStillInTrack) {
322                    fScrollTimer.stop();
323                } else {
324                    fScrollListener.actionPerformed(new ActionEvent(fScrollTimer, 0, ""));
325                    startTimer(false);
326                }
327            }
328        }
329
330        int getValueFromOffset(final int xOffset, final int yOffset, final int firstValue) {
331            final boolean isHoriz = isHorizontal();
332
333            // find the amount of pixels we've moved x & y (we only care about one)
334            final int offsetWeCareAbout = isHoriz ? xOffset : yOffset;
335
336            // now based on that floating point percentage compute the real scroller value.
337            final int visibleAmt = fScrollBar.getVisibleAmount();
338            final int max = fScrollBar.getMaximum();
339            final int min = fScrollBar.getMinimum();
340            final int extent = max - min;
341
342            // ask native to tell us what the new float that is a ratio of how much scrollable area
343            // we have moved (not the thumb area, just the scrollable). If the
344            // scroller goes 0-100 with a visible area of 20 we are getting a ratio of the
345            // remaining 80.
346            syncState(fScrollBar);
347            final double offsetChange = JRSUIUtils.ScrollBar.getNativeOffsetChange(painter.getControl(), 0, 0, fScrollBar.getWidth(), fScrollBar.getHeight(), offsetWeCareAbout, visibleAmt, extent);
348
349            // the scrollable area is the extent - visible amount;
350            final int scrollableArea = extent - visibleAmt;
351
352            final int changeByValue = (int)(offsetChange * scrollableArea);
353            int newValue = firstValue + changeByValue;
354            newValue = Math.max(min, newValue);
355            newValue = Math.min((max - visibleAmt), newValue);
356            return newValue;
357        }
358
359        /**
360         * Arrow Listeners
361         */
362        // Because we are handling both mousePressed and Actions
363        // we need to make sure we don't fire under both conditions.
364        // (keyfocus on scrollbars causes action without mousePress
365        void mousePressedInArrows(final MouseEvent e, final Hit part) {
366            final int direction = HitUtil.isIncrement(part) ? 1 : -1;
367
368            fStillInArrow = true;
369            scrollByUnit(direction);
370            fScrollTimer.stop();
371            fScrollListener.setDirection(direction);
372            fScrollListener.setScrollByBlock(false);
373
374            fMousePart = part;
375            startTimer(true);
376        }
377
378        void mouseReleasedInArrows(final MouseEvent e) {
379            fScrollTimer.stop();
380            fMousePart = Hit.NONE;
381            fScrollBar.setValueIsAdjusting(false);
382        }
383
384        void mouseDraggedInArrows(final MouseEvent e) {
385            final Hit whichPart = getPartHit(e.getX(), e.getY());
386
387            if ((fMousePart == whichPart) && fStillInArrow) return; // Nothing has changed, so return
388
389            if (fMousePart != whichPart && !HitUtil.isArrow(whichPart)) {
390                // The mouse is not over the arrow we mouse pressed in, so stop the timer and mark as
391                // not being in the arrow
392                fScrollTimer.stop();
393                fStillInArrow = false;
394                fScrollBar.repaint();
395            } else {
396                // We are in the arrow we mouse pressed down in originally, but the timer was stopped so we need
397                // to start it up again.
398                fMousePart = whichPart;
399                fScrollListener.setDirection(HitUtil.isIncrement(whichPart) ? 1 : -1);
400                fStillInArrow = true;
401                fScrollListener.actionPerformed(new ActionEvent(fScrollTimer, 0, ""));
402                startTimer(false);
403            }
404
405            fScrollBar.repaint();
406        }
407
408        void mouseReleasedInTrack(final MouseEvent e) {
409            if (fTrackHighlight != Hit.NONE) {
410                fScrollBar.repaint();
411            }
412
413            fTrackHighlight = Hit.NONE;
414            fIsDragging = false;
415            fScrollTimer.stop();
416            fScrollBar.setValueIsAdjusting(false);
417        }
418
419        /**
420         * Adjust the fScrollBars value based on the result of hitTestTrack
421         */
422        void mousePressedInTrack(final MouseEvent e, final Hit part) {
423            fScrollBar.setValueIsAdjusting(true);
424
425            // If option-click, toggle scroll-to-here
426            boolean shouldScrollToHere = (part != ScrollBarHit.THUMB) && JRSUIUtils.ScrollBar.useScrollToClick();
427            if (e.isAltDown()) shouldScrollToHere = !shouldScrollToHere;
428
429            // pretend the mouse was dragged from a point in the current thumb to the current mouse point in one big jump
430            if (shouldScrollToHere) {
431                final Point p = getScrollToHereStartPoint(e.getX(), e.getY());
432                fFirstMouseX = p.x;
433                fFirstMouseY = p.y;
434                fFirstValue = fScrollBar.getValue();
435                moveToMouse(e);
436
437                // OK, now we're in the thumb - any subsequent dragging should move it
438                fTrackHighlight = ScrollBarHit.THUMB;
439                fIsDragging = true;
440                return;
441            }
442
443            fCurrentMouseX = e.getX();
444            fCurrentMouseY = e.getY();
445
446            int direction = 0;
447            if (part == ScrollBarHit.TRACK_MIN) {
448                fTrackHighlight = ScrollBarHit.TRACK_MIN;
449                direction = -1;
450            } else if (part == ScrollBarHit.TRACK_MAX) {
451                fTrackHighlight = ScrollBarHit.TRACK_MAX;
452                direction = 1;
453            } else {
454                fFirstValue = fScrollBar.getValue();
455                fFirstMouseX = fCurrentMouseX;
456                fFirstMouseY = fCurrentMouseY;
457                fTrackHighlight = ScrollBarHit.THUMB;
458                fIsDragging = true;
459                return;
460            }
461
462            fIsDragging = false;
463            fStillInTrack = true;
464
465            scrollByBlock(direction);
466            // Check the new location of the thumb
467            // stop scrolling if the thumb is under the mouse??
468
469            final Hit newPart = getPartHit(fCurrentMouseX, fCurrentMouseY);
470            if (newPart == ScrollBarHit.TRACK_MIN || newPart == ScrollBarHit.TRACK_MAX) {
471                fScrollTimer.stop();
472                fScrollListener.setDirection(((newPart == ScrollBarHit.TRACK_MAX) ? 1 : -1));
473                fScrollListener.setScrollByBlock(true);
474                startTimer(true);
475            }
476        }
477
478        /**
479         * Set the models value to the position of the top/left
480         * of the thumb relative to the origin of the track.
481         */
482        void mouseDraggedInTrack(final MouseEvent e) {
483            moveToMouse(e);
484        }
485
486        // For normal mouse dragging or click-to-here
487        // fCurrentMouseX, fCurrentMouseY, and fFirstValue must be set
488        void moveToMouse(final MouseEvent e) {
489            fCurrentMouseX = e.getX();
490            fCurrentMouseY = e.getY();
491
492            final int oldValue = fScrollBar.getValue();
493            final int newValue = getValueFromOffset(fCurrentMouseX - fFirstMouseX, fCurrentMouseY - fFirstMouseY, fFirstValue);
494            if (newValue == oldValue) return;
495
496            fScrollBar.setValue(newValue);
497            final Rectangle dirtyRect = getTrackBounds();
498            fScrollBar.repaint(dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height);
499        }
500    }
501
502    /**
503     * Listener for scrolling events initiated in the ScrollPane.
504     */
505    protected class ScrollListener implements ActionListener {
506        boolean fUseBlockIncrement;
507        int fDirection = 1;
508
509        void setDirection(final int direction) {
510            this.fDirection = direction;
511        }
512
513        void setScrollByBlock(final boolean block) {
514            this.fUseBlockIncrement = block;
515        }
516
517        public void actionPerformed(final ActionEvent e) {
518            if (fUseBlockIncrement) {
519                Hit newPart = getPartHit(fTrackListener.fCurrentMouseX, fTrackListener.fCurrentMouseY);
520
521                if (newPart == ScrollBarHit.TRACK_MIN || newPart == ScrollBarHit.TRACK_MAX) {
522                    final int newDirection = (newPart == ScrollBarHit.TRACK_MAX ? 1 : -1);
523                    if (fDirection != newDirection) {
524                        fDirection = newDirection;
525                    }
526                }
527
528                scrollByBlock(fDirection);
529                newPart = getPartHit(fTrackListener.fCurrentMouseX, fTrackListener.fCurrentMouseY);
530
531                if (newPart == ScrollBarHit.THUMB) {
532                    ((Timer)e.getSource()).stop();
533                }
534            } else {
535                scrollByUnit(fDirection);
536            }
537
538            if (fDirection > 0 && fScrollBar.getValue() + fScrollBar.getVisibleAmount() >= fScrollBar.getMaximum()) {
539                ((Timer)e.getSource()).stop();
540            } else if (fDirection < 0 && fScrollBar.getValue() <= fScrollBar.getMinimum()) {
541                ((Timer)e.getSource()).stop();
542            }
543        }
544    }
545
546    float getThumbStart() {
547        final int max = fScrollBar.getMaximum();
548        final int min = fScrollBar.getMinimum();
549        final int extent = max - min;
550        if (extent <= 0) return 0f;
551
552        return (float)(fScrollBar.getValue() - fScrollBar.getMinimum()) / (float)extent;
553    }
554
555    float getThumbPercent() {
556        final int visible = fScrollBar.getVisibleAmount();
557        final int max = fScrollBar.getMaximum();
558        final int min = fScrollBar.getMinimum();
559        final int extent = max - min;
560        if (extent <= 0) return 0f;
561
562        return (float)visible / (float)extent;
563    }
564
565    /**
566     * A scrollbar's preferred width is 16 by a reasonable size to hold
567     * the arrows
568     *
569     * @param c The JScrollBar that's delegating this method to us.
570     * @return The preferred size of a Basic JScrollBar.
571     * @see #getMaximumSize
572     * @see #getMinimumSize
573     */
574    public Dimension getPreferredSize(final JComponent c) {
575        return isHorizontal() ? new Dimension(96, 15) : new Dimension(15, 96);
576    }
577
578    public Dimension getMinimumSize(final JComponent c) {
579        return isHorizontal() ? new Dimension(54, 15) : new Dimension(15, 54);
580    }
581
582    public Dimension getMaximumSize(final JComponent c) {
583        return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
584    }
585
586    boolean isHorizontal() {
587        return fScrollBar.getOrientation() == Adjustable.HORIZONTAL;
588    }
589
590    // only do scroll-to-here for page up and page down regions, when the option key is pressed
591    // This gets the point where the mouse would have been clicked in the current thumb
592    // so we can pretend the mouse was dragged to the current mouse point in one big jump
593    Point getScrollToHereStartPoint(final int clickPosX, final int clickPosY) {
594        // prepare the track rectangle and limit rectangle so we can do our calculations
595        final Rectangle limitRect = getDragBounds(); // GetThemeTrackDragRect
596
597        // determine the bounding rectangle for our thumb region
598        syncState(fScrollBar);
599        double[] rect = new double[4];
600        JRSUIUtils.ScrollBar.getPartBounds(rect, painter.getControl(), 0, 0, fScrollBar.getWidth(), fScrollBar.getHeight(), ScrollBarPart.THUMB);
601        final Rectangle r = new Rectangle((int)rect[0], (int)rect[1], (int)rect[2], (int)rect[3]);
602
603        // figure out the scroll-to-here start location based on our orientation, the
604        // click position, and where it must be in the thumb to travel to the endpoints
605        // properly.
606        final Point startPoint = new Point(clickPosX, clickPosY);
607
608        if (isHorizontal()) {
609            final int halfWidth = r.width / 2;
610            final int limitRectRight = limitRect.x + limitRect.width;
611
612            if (clickPosX + halfWidth > limitRectRight) {
613                // Up against right edge
614                startPoint.x = r.x + r.width - limitRectRight - clickPosX - 1;
615            } else if (clickPosX - halfWidth < limitRect.x) {
616                // Up against left edge
617                startPoint.x = r.x + clickPosX - limitRect.x;
618            } else {
619                // Center the thumb
620                startPoint.x = r.x + halfWidth;
621            }
622
623            // Pretend clicked in middle of indicator vertically
624            startPoint.y = (r.y + r.height) / 2;
625            return startPoint;
626        }
627
628        final int halfHeight = r.height / 2;
629        final int limitRectBottom = limitRect.y + limitRect.height;
630
631        if (clickPosY + halfHeight > limitRectBottom) {
632            // Up against bottom edge
633            startPoint.y = r.y + r.height - limitRectBottom - clickPosY - 1;
634        } else if (clickPosY - halfHeight < limitRect.y) {
635            // Up against top edge
636            startPoint.y = r.y + clickPosY - limitRect.y;
637        } else {
638            // Center the thumb
639            startPoint.y = r.y + halfHeight;
640        }
641
642        // Pretend clicked in middle of indicator horizontally
643        startPoint.x = (r.x + r.width) / 2;
644
645        return startPoint;
646    }
647
648    static class HitUtil {
649        static boolean isIncrement(final Hit hit) {
650            return (hit == ScrollBarHit.ARROW_MAX) || (hit == ScrollBarHit.ARROW_MAX_INSIDE);
651        }
652
653        static boolean isDecrement(final Hit hit) {
654            return (hit == ScrollBarHit.ARROW_MIN) || (hit == ScrollBarHit.ARROW_MIN_INSIDE);
655        }
656
657        static boolean isArrow(final Hit hit) {
658            return isIncrement(hit) || isDecrement(hit);
659        }
660
661        static boolean isTrack(final Hit hit) {
662            return (hit == ScrollBarHit.TRACK_MAX) || (hit == ScrollBarHit.TRACK_MIN);
663        }
664    }
665}
666