/*
 * Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.apple.laf;

import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.util.*;

import javax.swing.*;
import javax.swing.Timer;
import javax.swing.event.*;
import javax.swing.plaf.*;

import apple.laf.*;
import apple.laf.JRSUIConstants.*;
import apple.laf.JRSUIState.ScrollBarState;

import com.apple.laf.AquaUtils.RecyclableSingleton;

public class AquaScrollBarUI extends ScrollBarUI {
    private static final int kInitialDelay = 300;
    private static final int kNormalDelay = 100;

    // when we make small and mini scrollbars, this will no longer be a constant
    static final int MIN_ARROW_COLLAPSE_SIZE = 64;

    // tracking state
    protected boolean fIsDragging;
    protected Timer fScrollTimer;
    protected ScrollListener fScrollListener;
    protected TrackListener fTrackListener;
    protected Hit fTrackHighlight = Hit.NONE;
    protected Hit fMousePart = Hit.NONE; // Which arrow (if any) we moused pressed down in (used by arrow drag tracking)

    protected JScrollBar fScrollBar;
    protected ModelListener fModelListener;
    protected PropertyChangeListener fPropertyChangeListener;

    protected final AquaPainter<ScrollBarState> painter = AquaPainter.create(JRSUIStateFactory.getScrollBar());

    // Create PLAF
    public static ComponentUI createUI(final JComponent c) {
        return new AquaScrollBarUI();
    }

    public AquaScrollBarUI() { }

    public void installUI(final JComponent c) {
        fScrollBar = (JScrollBar)c;
        installListeners();
        configureScrollBarColors();
    }

    public void uninstallUI(final JComponent c) {
        uninstallListeners();
        fScrollBar = null;
    }

    protected void configureScrollBarColors() {
        LookAndFeel.installColors(fScrollBar, "ScrollBar.background", "ScrollBar.foreground");
    }

    protected TrackListener createTrackListener() {
        return new TrackListener();
    }

    protected ScrollListener createScrollListener() {
        return new ScrollListener();
    }

    protected void installListeners() {
        fTrackListener = createTrackListener();
        fModelListener = createModelListener();
        fPropertyChangeListener = createPropertyChangeListener();
        fScrollBar.addMouseListener(fTrackListener);
        fScrollBar.addMouseMotionListener(fTrackListener);
        fScrollBar.getModel().addChangeListener(fModelListener);
        fScrollBar.addPropertyChangeListener(fPropertyChangeListener);
        fScrollListener = createScrollListener();
        fScrollTimer = new Timer(kNormalDelay, fScrollListener);
        fScrollTimer.setInitialDelay(kInitialDelay); // default InitialDelay?
    }

    protected void uninstallListeners() {
        fScrollTimer.stop();
        fScrollTimer = null;
        fScrollBar.getModel().removeChangeListener(fModelListener);
        fScrollBar.removeMouseListener(fTrackListener);
        fScrollBar.removeMouseMotionListener(fTrackListener);
        fScrollBar.removePropertyChangeListener(fPropertyChangeListener);
    }

    protected PropertyChangeListener createPropertyChangeListener() {
        return new PropertyChangeHandler();
    }

    protected ModelListener createModelListener() {
        return new ModelListener();
    }

    protected void syncState(final JComponent c) {
        final ScrollBarState scrollBarState = painter.state;
        scrollBarState.set(isHorizontal() ? Orientation.HORIZONTAL : Orientation.VERTICAL);

        final float trackExtent = fScrollBar.getMaximum() - fScrollBar.getMinimum() - fScrollBar.getModel().getExtent();
        if (trackExtent <= 0.0f) {
            scrollBarState.set(NothingToScroll.YES);
            return;
        }

        final ScrollBarPart pressedPart = getPressedPart();
        scrollBarState.set(pressedPart);
        scrollBarState.set(getState(c, pressedPart));
        scrollBarState.set(NothingToScroll.NO);
        scrollBarState.setValue((fScrollBar.getValue() - fScrollBar.getMinimum()) / trackExtent);
        scrollBarState.setThumbStart(getThumbStart());
        scrollBarState.setThumbPercent(getThumbPercent());
        scrollBarState.set(shouldShowArrows() ? ShowArrows.YES : ShowArrows.NO);
    }

    public void paint(final Graphics g, final JComponent c) {
        syncState(c);
        painter.paint(g, c, 0, 0, fScrollBar.getWidth(), fScrollBar.getHeight());
    }

    protected State getState(final JComponent c, final ScrollBarPart pressedPart) {
        if (!AquaFocusHandler.isActive(c)) return State.INACTIVE;
        if (!c.isEnabled()) return State.INACTIVE;
        if (pressedPart != ScrollBarPart.NONE) return State.PRESSED;
        return State.ACTIVE;
    }

    private static final RecyclableSingleton<Map<Hit, ScrollBarPart>> hitToPressedPartMap = new RecyclableSingleton<Map<Hit,ScrollBarPart>>(){
        @Override
        protected Map<Hit, ScrollBarPart> getInstance() {
            final Map<Hit, ScrollBarPart> map = new HashMap<Hit, ScrollBarPart>(7);
            map.put(ScrollBarHit.ARROW_MAX, ScrollBarPart.ARROW_MAX);
            map.put(ScrollBarHit.ARROW_MIN, ScrollBarPart.ARROW_MIN);
            map.put(ScrollBarHit.ARROW_MAX_INSIDE, ScrollBarPart.ARROW_MAX_INSIDE);
            map.put(ScrollBarHit.ARROW_MIN_INSIDE, ScrollBarPart.ARROW_MIN_INSIDE);
            map.put(ScrollBarHit.TRACK_MAX, ScrollBarPart.TRACK_MAX);
            map.put(ScrollBarHit.TRACK_MIN, ScrollBarPart.TRACK_MIN);
            map.put(ScrollBarHit.THUMB, ScrollBarPart.THUMB);
            return map;
        }
    };
    protected ScrollBarPart getPressedPart() {
        if (!fTrackListener.fInArrows || !fTrackListener.fStillInArrow) return ScrollBarPart.NONE;
        final ScrollBarPart pressedPart = hitToPressedPartMap.get().get(fMousePart);
        if (pressedPart == null) return ScrollBarPart.NONE;
        return pressedPart;
    }

    protected boolean shouldShowArrows() {
        return MIN_ARROW_COLLAPSE_SIZE < (isHorizontal() ? fScrollBar.getWidth() : fScrollBar.getHeight());
    }

    // Layout Methods
    // Layout is controlled by the user in the Appearance Control Panel
    // Theme will redraw correctly for the current layout
    public void layoutContainer(final Container fScrollBarContainer) {
        fScrollBar.repaint();
        fScrollBar.revalidate();
    }

    protected Rectangle getTrackBounds() {
        return new Rectangle(0, 0, fScrollBar.getWidth(), fScrollBar.getHeight());
    }

    protected Rectangle getDragBounds() {
        return new Rectangle(0, 0, fScrollBar.getWidth(), fScrollBar.getHeight());
    }

    protected void startTimer(final boolean initial) {
        fScrollTimer.setInitialDelay(initial ? kInitialDelay : kNormalDelay); // default InitialDelay?
        fScrollTimer.start();
    }

    protected void scrollByBlock(final int direction) {
        synchronized(fScrollBar) {
            final int oldValue = fScrollBar.getValue();
            final int blockIncrement = fScrollBar.getBlockIncrement(direction);
            final int delta = blockIncrement * ((direction > 0) ? +1 : -1);

            fScrollBar.setValue(oldValue + delta);
            fTrackHighlight = direction > 0 ? ScrollBarHit.TRACK_MAX : ScrollBarHit.TRACK_MIN;
            fScrollBar.repaint();
            fScrollListener.setDirection(direction);
            fScrollListener.setScrollByBlock(true);
        }
    }

    protected void scrollByUnit(final int direction) {
        synchronized(fScrollBar) {
            int delta = fScrollBar.getUnitIncrement(direction);
            if (direction <= 0) delta = -delta;

            fScrollBar.setValue(delta + fScrollBar.getValue());
            fScrollBar.repaint();
            fScrollListener.setDirection(direction);
            fScrollListener.setScrollByBlock(false);
        }
    }

    protected Hit getPartHit(final int x, final int y) {
        syncState(fScrollBar);
        return JRSUIUtils.HitDetection.getHitForPoint(painter.getControl(), 0, 0, fScrollBar.getWidth(), fScrollBar.getHeight(), x, y);
    }

    protected class PropertyChangeHandler implements PropertyChangeListener {
        public void propertyChange(final PropertyChangeEvent e) {
            final String propertyName = e.getPropertyName();

            if ("model".equals(propertyName)) {
                final BoundedRangeModel oldModel = (BoundedRangeModel)e.getOldValue();
                final BoundedRangeModel newModel = (BoundedRangeModel)e.getNewValue();
                oldModel.removeChangeListener(fModelListener);
                newModel.addChangeListener(fModelListener);
                fScrollBar.repaint();
                fScrollBar.revalidate();
            } else if (AquaFocusHandler.FRAME_ACTIVE_PROPERTY.equals(propertyName)) {
                fScrollBar.repaint();
            }
        }
    }

    protected class ModelListener implements ChangeListener {
        public void stateChanged(final ChangeEvent e) {
            layoutContainer(fScrollBar);
        }
    }

    // Track mouse drags.
    protected class TrackListener extends MouseAdapter implements MouseMotionListener {
        protected transient int fCurrentMouseX, fCurrentMouseY;
        protected transient boolean fInArrows; // are we currently tracking arrows?
        protected transient boolean fStillInArrow = false; // Whether mouse is in an arrow during arrow tracking
        protected transient boolean fStillInTrack = false; // Whether mouse is in the track during pageup/down tracking
        protected transient int fFirstMouseX, fFirstMouseY, fFirstValue; // Values for getValueFromOffset

        public void mouseReleased(final MouseEvent e) {
            if (!fScrollBar.isEnabled()) return;
            if (fInArrows) {
                mouseReleasedInArrows(e);
            } else {
                mouseReleasedInTrack(e);
            }

            fInArrows = false;
            fStillInArrow = false;
            fStillInTrack = false;

            fScrollBar.repaint();
            fScrollBar.revalidate();
        }

        public void mousePressed(final MouseEvent e) {
            if (!fScrollBar.isEnabled()) return;

            final Hit part = getPartHit(e.getX(), e.getY());
            fInArrows = HitUtil.isArrow(part);
            if (fInArrows) {
                mousePressedInArrows(e, part);
            } else {
                if (part == Hit.NONE) {
                    fTrackHighlight = Hit.NONE;
                } else {
                    mousePressedInTrack(e, part);
                }
            }
        }

        public void mouseDragged(final MouseEvent e) {
            if (!fScrollBar.isEnabled()) return;

            if (fInArrows) {
                mouseDraggedInArrows(e);
            } else if (fIsDragging) {
                mouseDraggedInTrack(e);
            } else {
                // In pageup/down zones

                // check that thumb has not been scrolled under the mouse cursor
                final Hit previousPart = getPartHit(fCurrentMouseX, fCurrentMouseY);
                if (!HitUtil.isTrack(previousPart)) {
                    fStillInTrack = false;
                }

                fCurrentMouseX = e.getX();
                fCurrentMouseY = e.getY();

                final Hit part = getPartHit(e.getX(), e.getY());
                final boolean temp = HitUtil.isTrack(part);
                if (temp == fStillInTrack) return;

                fStillInTrack = temp;
                if (!fStillInTrack) {
                    fScrollTimer.stop();
                } else {
                    fScrollListener.actionPerformed(new ActionEvent(fScrollTimer, 0, ""));
                    startTimer(false);
                }
            }
        }

        int getValueFromOffset(final int xOffset, final int yOffset, final int firstValue) {
            final boolean isHoriz = isHorizontal();

            // find the amount of pixels we've moved x & y (we only care about one)
            final int offsetWeCareAbout = isHoriz ? xOffset : yOffset;

            // now based on that floating point percentage compute the real scroller value.
            final int visibleAmt = fScrollBar.getVisibleAmount();
            final int max = fScrollBar.getMaximum();
            final int min = fScrollBar.getMinimum();
            final int extent = max - min;

            // ask native to tell us what the new float that is a ratio of how much scrollable area
            // we have moved (not the thumb area, just the scrollable). If the
            // scroller goes 0-100 with a visible area of 20 we are getting a ratio of the
            // remaining 80.
            syncState(fScrollBar);
            final double offsetChange = JRSUIUtils.ScrollBar.getNativeOffsetChange(painter.getControl(), 0, 0, fScrollBar.getWidth(), fScrollBar.getHeight(), offsetWeCareAbout, visibleAmt, extent);

            // the scrollable area is the extent - visible amount;
            final int scrollableArea = extent - visibleAmt;

            final int changeByValue = (int)(offsetChange * scrollableArea);
            int newValue = firstValue + changeByValue;
            newValue = Math.max(min, newValue);
            newValue = Math.min((max - visibleAmt), newValue);
            return newValue;
        }

        
Arrow Listeners
/** * Arrow Listeners */
// Because we are handling both mousePressed and Actions // we need to make sure we don't fire under both conditions. // (keyfocus on scrollbars causes action without mousePress void mousePressedInArrows(final MouseEvent e, final Hit part) { final int direction = HitUtil.isIncrement(part) ? 1 : -1; fStillInArrow = true; scrollByUnit(direction); fScrollTimer.stop(); fScrollListener.setDirection(direction); fScrollListener.setScrollByBlock(false); fMousePart = part; startTimer(true); } void mouseReleasedInArrows(final MouseEvent e) { fScrollTimer.stop(); fMousePart = Hit.NONE; fScrollBar.setValueIsAdjusting(false); } void mouseDraggedInArrows(final MouseEvent e) { final Hit whichPart = getPartHit(e.getX(), e.getY()); if ((fMousePart == whichPart) && fStillInArrow) return; // Nothing has changed, so return if (fMousePart != whichPart && !HitUtil.isArrow(whichPart)) { // The mouse is not over the arrow we mouse pressed in, so stop the timer and mark as // not being in the arrow fScrollTimer.stop(); fStillInArrow = false; fScrollBar.repaint(); } else { // We are in the arrow we mouse pressed down in originally, but the timer was stopped so we need // to start it up again. fMousePart = whichPart; fScrollListener.setDirection(HitUtil.isIncrement(whichPart) ? 1 : -1); fStillInArrow = true; fScrollListener.actionPerformed(new ActionEvent(fScrollTimer, 0, "")); startTimer(false); } fScrollBar.repaint(); } void mouseReleasedInTrack(final MouseEvent e) { if (fTrackHighlight != Hit.NONE) { fScrollBar.repaint(); } fTrackHighlight = Hit.NONE; fIsDragging = false; fScrollTimer.stop(); fScrollBar.setValueIsAdjusting(false); }
Adjust the fScrollBars value based on the result of hitTestTrack
/** * Adjust the fScrollBars value based on the result of hitTestTrack */
void mousePressedInTrack(final MouseEvent e, final Hit part) { fScrollBar.setValueIsAdjusting(true); // If option-click, toggle scroll-to-here boolean shouldScrollToHere = (part != ScrollBarHit.THUMB) && JRSUIUtils.ScrollBar.useScrollToClick(); if (e.isAltDown()) shouldScrollToHere = !shouldScrollToHere; // pretend the mouse was dragged from a point in the current thumb to the current mouse point in one big jump if (shouldScrollToHere) { final Point p = getScrollToHereStartPoint(e.getX(), e.getY()); fFirstMouseX = p.x; fFirstMouseY = p.y; fFirstValue = fScrollBar.getValue(); moveToMouse(e); // OK, now we're in the thumb - any subsequent dragging should move it fTrackHighlight = ScrollBarHit.THUMB; fIsDragging = true; return; } fCurrentMouseX = e.getX(); fCurrentMouseY = e.getY(); int direction = 0; if (part == ScrollBarHit.TRACK_MIN) { fTrackHighlight = ScrollBarHit.TRACK_MIN; direction = -1; } else if (part == ScrollBarHit.TRACK_MAX) { fTrackHighlight = ScrollBarHit.TRACK_MAX; direction = 1; } else { fFirstValue = fScrollBar.getValue(); fFirstMouseX = fCurrentMouseX; fFirstMouseY = fCurrentMouseY; fTrackHighlight = ScrollBarHit.THUMB; fIsDragging = true; return; } fIsDragging = false; fStillInTrack = true; scrollByBlock(direction); // Check the new location of the thumb // stop scrolling if the thumb is under the mouse?? final Hit newPart = getPartHit(fCurrentMouseX, fCurrentMouseY); if (newPart == ScrollBarHit.TRACK_MIN || newPart == ScrollBarHit.TRACK_MAX) { fScrollTimer.stop(); fScrollListener.setDirection(((newPart == ScrollBarHit.TRACK_MAX) ? 1 : -1)); fScrollListener.setScrollByBlock(true); startTimer(true); } }
Set the models value to the position of the top/left of the thumb relative to the origin of the track.
/** * Set the models value to the position of the top/left * of the thumb relative to the origin of the track. */
void mouseDraggedInTrack(final MouseEvent e) { moveToMouse(e); } // For normal mouse dragging or click-to-here // fCurrentMouseX, fCurrentMouseY, and fFirstValue must be set void moveToMouse(final MouseEvent e) { fCurrentMouseX = e.getX(); fCurrentMouseY = e.getY(); final int oldValue = fScrollBar.getValue(); final int newValue = getValueFromOffset(fCurrentMouseX - fFirstMouseX, fCurrentMouseY - fFirstMouseY, fFirstValue); if (newValue == oldValue) return; fScrollBar.setValue(newValue); final Rectangle dirtyRect = getTrackBounds(); fScrollBar.repaint(dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height); } }
Listener for scrolling events initiated in the ScrollPane.
/** * Listener for scrolling events initiated in the ScrollPane. */
protected class ScrollListener implements ActionListener { boolean fUseBlockIncrement; int fDirection = 1; void setDirection(final int direction) { this.fDirection = direction; } void setScrollByBlock(final boolean block) { this.fUseBlockIncrement = block; } public void actionPerformed(final ActionEvent e) { if (fUseBlockIncrement) { Hit newPart = getPartHit(fTrackListener.fCurrentMouseX, fTrackListener.fCurrentMouseY); if (newPart == ScrollBarHit.TRACK_MIN || newPart == ScrollBarHit.TRACK_MAX) { final int newDirection = (newPart == ScrollBarHit.TRACK_MAX ? 1 : -1); if (fDirection != newDirection) { fDirection = newDirection; } } scrollByBlock(fDirection); newPart = getPartHit(fTrackListener.fCurrentMouseX, fTrackListener.fCurrentMouseY); if (newPart == ScrollBarHit.THUMB) { ((Timer)e.getSource()).stop(); } } else { scrollByUnit(fDirection); } if (fDirection > 0 && fScrollBar.getValue() + fScrollBar.getVisibleAmount() >= fScrollBar.getMaximum()) { ((Timer)e.getSource()).stop(); } else if (fDirection < 0 && fScrollBar.getValue() <= fScrollBar.getMinimum()) { ((Timer)e.getSource()).stop(); } } } float getThumbStart() { final int max = fScrollBar.getMaximum(); final int min = fScrollBar.getMinimum(); final int extent = max - min; if (extent <= 0) return 0f; return (float)(fScrollBar.getValue() - fScrollBar.getMinimum()) / (float)extent; } float getThumbPercent() { final int visible = fScrollBar.getVisibleAmount(); final int max = fScrollBar.getMaximum(); final int min = fScrollBar.getMinimum(); final int extent = max - min; if (extent <= 0) return 0f; return (float)visible / (float)extent; }
A scrollbar's preferred width is 16 by a reasonable size to hold the arrows
Params:
  • c – The JScrollBar that's delegating this method to us.
See Also:
Returns:The preferred size of a Basic JScrollBar.
/** * A scrollbar's preferred width is 16 by a reasonable size to hold * the arrows * * @param c The JScrollBar that's delegating this method to us. * @return The preferred size of a Basic JScrollBar. * @see #getMaximumSize * @see #getMinimumSize */
public Dimension getPreferredSize(final JComponent c) { return isHorizontal() ? new Dimension(96, 15) : new Dimension(15, 96); } public Dimension getMinimumSize(final JComponent c) { return isHorizontal() ? new Dimension(54, 15) : new Dimension(15, 54); } public Dimension getMaximumSize(final JComponent c) { return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); } boolean isHorizontal() { return fScrollBar.getOrientation() == Adjustable.HORIZONTAL; } // only do scroll-to-here for page up and page down regions, when the option key is pressed // This gets the point where the mouse would have been clicked in the current thumb // so we can pretend the mouse was dragged to the current mouse point in one big jump Point getScrollToHereStartPoint(final int clickPosX, final int clickPosY) { // prepare the track rectangle and limit rectangle so we can do our calculations final Rectangle limitRect = getDragBounds(); // GetThemeTrackDragRect // determine the bounding rectangle for our thumb region syncState(fScrollBar); double[] rect = new double[4]; JRSUIUtils.ScrollBar.getPartBounds(rect, painter.getControl(), 0, 0, fScrollBar.getWidth(), fScrollBar.getHeight(), ScrollBarPart.THUMB); final Rectangle r = new Rectangle((int)rect[0], (int)rect[1], (int)rect[2], (int)rect[3]); // figure out the scroll-to-here start location based on our orientation, the // click position, and where it must be in the thumb to travel to the endpoints // properly. final Point startPoint = new Point(clickPosX, clickPosY); if (isHorizontal()) { final int halfWidth = r.width / 2; final int limitRectRight = limitRect.x + limitRect.width; if (clickPosX + halfWidth > limitRectRight) { // Up against right edge startPoint.x = r.x + r.width - limitRectRight - clickPosX - 1; } else if (clickPosX - halfWidth < limitRect.x) { // Up against left edge startPoint.x = r.x + clickPosX - limitRect.x; } else { // Center the thumb startPoint.x = r.x + halfWidth; } // Pretend clicked in middle of indicator vertically startPoint.y = (r.y + r.height) / 2; return startPoint; } final int halfHeight = r.height / 2; final int limitRectBottom = limitRect.y + limitRect.height; if (clickPosY + halfHeight > limitRectBottom) { // Up against bottom edge startPoint.y = r.y + r.height - limitRectBottom - clickPosY - 1; } else if (clickPosY - halfHeight < limitRect.y) { // Up against top edge startPoint.y = r.y + clickPosY - limitRect.y; } else { // Center the thumb startPoint.y = r.y + halfHeight; } // Pretend clicked in middle of indicator horizontally startPoint.x = (r.x + r.width) / 2; return startPoint; } static class HitUtil { static boolean isIncrement(final Hit hit) { return (hit == ScrollBarHit.ARROW_MAX) || (hit == ScrollBarHit.ARROW_MAX_INSIDE); } static boolean isDecrement(final Hit hit) { return (hit == ScrollBarHit.ARROW_MIN) || (hit == ScrollBarHit.ARROW_MIN_INSIDE); } static boolean isArrow(final Hit hit) { return isIncrement(hit) || isDecrement(hit); } static boolean isTrack(final Hit hit) { return (hit == ScrollBarHit.TRACK_MAX) || (hit == ScrollBarHit.TRACK_MIN); } } }