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.*;
30 import java.beans.*;
31 import java.util.*;
32 
33 import javax.swing.*;
34 import javax.swing.Timer;
35 import javax.swing.event.*;
36 import javax.swing.plaf.*;
37 
38 import apple.laf.*;
39 import apple.laf.JRSUIConstants.*;
40 import apple.laf.JRSUIState.ScrollBarState;
41 
42 import com.apple.laf.AquaUtils.RecyclableSingleton;
43 
44 public 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
createUI(final JComponent c)66     public static ComponentUI createUI(final JComponent c) {
67         return new AquaScrollBarUI();
68     }
69 
AquaScrollBarUI()70     public AquaScrollBarUI() { }
71 
installUI(final JComponent c)72     public void installUI(final JComponent c) {
73         fScrollBar = (JScrollBar)c;
74         installListeners();
75         configureScrollBarColors();
76     }
77 
uninstallUI(final JComponent c)78     public void uninstallUI(final JComponent c) {
79         uninstallListeners();
80         fScrollBar = null;
81     }
82 
configureScrollBarColors()83     protected void configureScrollBarColors() {
84         LookAndFeel.installColors(fScrollBar, "ScrollBar.background", "ScrollBar.foreground");
85     }
86 
createTrackListener()87     protected TrackListener createTrackListener() {
88         return new TrackListener();
89     }
90 
createScrollListener()91     protected ScrollListener createScrollListener() {
92         return new ScrollListener();
93     }
94 
installListeners()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 
uninstallListeners()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 
createPropertyChangeListener()117     protected PropertyChangeListener createPropertyChangeListener() {
118         return new PropertyChangeHandler();
119     }
120 
createModelListener()121     protected ModelListener createModelListener() {
122         return new ModelListener();
123     }
124 
syncState(final JComponent c)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 
paint(final Graphics g, final JComponent c)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 
getState(final JComponent c, final ScrollBarPart pressedPart)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     };
getPressedPart()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 
shouldShowArrows()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
layoutContainer(final Container fScrollBarContainer)185     public void layoutContainer(final Container fScrollBarContainer) {
186         fScrollBar.repaint();
187         fScrollBar.revalidate();
188     }
189 
getTrackBounds()190     protected Rectangle getTrackBounds() {
191         return new Rectangle(0, 0, fScrollBar.getWidth(), fScrollBar.getHeight());
192     }
193 
getDragBounds()194     protected Rectangle getDragBounds() {
195         return new Rectangle(0, 0, fScrollBar.getWidth(), fScrollBar.getHeight());
196     }
197 
startTimer(final boolean initial)198     protected void startTimer(final boolean initial) {
199         fScrollTimer.setInitialDelay(initial ? kInitialDelay : kNormalDelay); // default InitialDelay?
200         fScrollTimer.start();
201     }
202 
scrollByBlock(final int direction)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 
scrollByUnit(final int direction)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 
getPartHit(final int x, final int y)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 {
propertyChange(final PropertyChangeEvent e)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 {
stateChanged(final ChangeEvent e)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 
mouseReleased(final MouseEvent e)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 
mousePressed(final MouseEvent e)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 
mouseDragged(final MouseEvent e)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 
getValueFromOffset(final int xOffset, final int yOffset, final int firstValue)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
mousePressedInArrows(final MouseEvent e, final Hit part)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 
mouseReleasedInArrows(final MouseEvent e)378         void mouseReleasedInArrows(final MouseEvent e) {
379             fScrollTimer.stop();
380             fMousePart = Hit.NONE;
381             fScrollBar.setValueIsAdjusting(false);
382         }
383 
mouseDraggedInArrows(final MouseEvent e)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 
mouseReleasedInTrack(final MouseEvent e)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          */
mousePressedInTrack(final MouseEvent e, final Hit part)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          */
mouseDraggedInTrack(final MouseEvent e)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
moveToMouse(final MouseEvent e)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 
setDirection(final int direction)509         void setDirection(final int direction) {
510             this.fDirection = direction;
511         }
512 
setScrollByBlock(final boolean block)513         void setScrollByBlock(final boolean block) {
514             this.fUseBlockIncrement = block;
515         }
516 
actionPerformed(final ActionEvent e)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 
getThumbStart()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 
getThumbPercent()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      */
getPreferredSize(final JComponent c)574     public Dimension getPreferredSize(final JComponent c) {
575         return isHorizontal() ? new Dimension(96, 15) : new Dimension(15, 96);
576     }
577 
getMinimumSize(final JComponent c)578     public Dimension getMinimumSize(final JComponent c) {
579         return isHorizontal() ? new Dimension(54, 15) : new Dimension(15, 54);
580     }
581 
getMaximumSize(final JComponent c)582     public Dimension getMaximumSize(final JComponent c) {
583         return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
584     }
585 
isHorizontal()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
getScrollToHereStartPoint(final int clickPosX, final int clickPosY)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 {
isIncrement(final Hit hit)649         static boolean isIncrement(final Hit hit) {
650             return (hit == ScrollBarHit.ARROW_MAX) || (hit == ScrollBarHit.ARROW_MAX_INSIDE);
651         }
652 
isDecrement(final Hit hit)653         static boolean isDecrement(final Hit hit) {
654             return (hit == ScrollBarHit.ARROW_MIN) || (hit == ScrollBarHit.ARROW_MIN_INSIDE);
655         }
656 
isArrow(final Hit hit)657         static boolean isArrow(final Hit hit) {
658             return isIncrement(hit) || isDecrement(hit);
659         }
660 
isTrack(final Hit hit)661         static boolean isTrack(final Hit hit) {
662             return (hit == ScrollBarHit.TRACK_MAX) || (hit == ScrollBarHit.TRACK_MIN);
663         }
664     }
665 }
666