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;
static final int MIN_ARROW_COLLAPSE_SIZE = 64;
protected boolean fIsDragging;
protected Timer fScrollTimer;
protected ScrollListener fScrollListener;
protected TrackListener fTrackListener;
protected Hit fTrackHighlight = Hit.NONE;
protected Hit fMousePart = Hit.NONE;
protected JScrollBar fScrollBar;
protected ModelListener fModelListener;
protected PropertyChangeListener fPropertyChangeListener;
protected final AquaPainter<ScrollBarState> painter = AquaPainter.create(JRSUIStateFactory.getScrollBar());
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);
}
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());
}
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);
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);
}
}
protected class TrackListener extends MouseAdapter implements MouseMotionListener {
protected transient int fCurrentMouseX, fCurrentMouseY;
protected transient boolean fInArrows;
protected transient boolean fStillInArrow = false;
protected transient boolean fStillInTrack = false;
protected transient int fFirstMouseX, fFirstMouseY, fFirstValue;
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 {
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();
final int offsetWeCareAbout = isHoriz ? xOffset : yOffset;
final int visibleAmt = fScrollBar.getVisibleAmount();
final int max = fScrollBar.getMaximum();
final int min = fScrollBar.getMinimum();
final int extent = max - min;
syncState(fScrollBar);
final double offsetChange = JRSUIUtils.ScrollBar.getNativeOffsetChange(painter.getControl(), 0, 0, fScrollBar.getWidth(), fScrollBar.getHeight(), offsetWeCareAbout, visibleAmt, extent);
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;
}
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;
if (fMousePart != whichPart && !HitUtil.isArrow(whichPart)) {
fScrollTimer.stop();
fStillInArrow = false;
fScrollBar.repaint();
} else {
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);
}
void mousePressedInTrack(final MouseEvent e, final Hit part) {
fScrollBar.setValueIsAdjusting(true);
boolean shouldScrollToHere = (part != ScrollBarHit.THUMB) && JRSUIUtils.ScrollBar.useScrollToClick();
if (e.isAltDown()) shouldScrollToHere = !shouldScrollToHere;
if (shouldScrollToHere) {
final Point p = getScrollToHereStartPoint(e.getX(), e.getY());
fFirstMouseX = p.x;
fFirstMouseY = p.y;
fFirstValue = fScrollBar.getValue();
moveToMouse(e);
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);
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);
}
}
void mouseDraggedInTrack(final MouseEvent e) {
moveToMouse(e);
}
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);
}
}
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;
}
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;
}
Point getScrollToHereStartPoint(final int clickPosX, final int clickPosY) {
final Rectangle limitRect = getDragBounds();
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]);
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) {
startPoint.x = r.x + r.width - limitRectRight - clickPosX - 1;
} else if (clickPosX - halfWidth < limitRect.x) {
startPoint.x = r.x + clickPosX - limitRect.x;
} else {
startPoint.x = r.x + halfWidth;
}
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) {
startPoint.y = r.y + r.height - limitRectBottom - clickPosY - 1;
} else if (clickPosY - halfHeight < limitRect.y) {
startPoint.y = r.y + clickPosY - limitRect.y;
} else {
startPoint.y = r.y + halfHeight;
}
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);
}
}
}