/* * Copyright (c) 2011, 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; } 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 */ // 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 */ 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. */ 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. */ 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 * * @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); } } }