package com.explodingpixels.widgets.plaf;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import javax.swing.BoundedRangeModel;
import javax.swing.JComponent;
import javax.swing.JScrollBar;
import javax.swing.SwingUtilities;
import javax.swing.plaf.basic.BasicScrollBarUI;
import com.explodingpixels.widgets.WindowUtils;
/**
* An implementation of {@link javax.swing.plaf.ScrollBarUI} that supports dynamic skinning.
* painting is delegated to a {@link com.explodingpixels.widgets.plaf.ScrollBarSkin}.
*/
public class SkinnableScrollBarUI extends BasicScrollBarUI {
private ScrollBarSkin fSkin;
private ScrollBarOrientation fOrientation;
private final ScrollBarSkinProvider fScrollBarSkinProvider;
/**
* Creates a {@code SkinnableScrollBarUI} that query the given {@link com.explodingpixels.widgets.plaf.SkinnableScrollBarUI.ScrollBarSkinProvider} in
* order to get the {@link com.explodingpixels.widgets.plaf.ScrollBarSkin} during the installation of this UI delegate.
*
* @param scrollBarSkinProvider the provider of the {@code ScrollBarSkin}.
*/
public SkinnableScrollBarUI(ScrollBarSkinProvider scrollBarSkinProvider) {
fScrollBarSkinProvider = scrollBarSkinProvider;
}
@Override
public void installUI(JComponent c) {
JScrollBar scrollBar = (JScrollBar) c;
// convert the Swing scroll bar orientation to the type-safe ScrollBarOrientation.
fOrientation = ScrollBarOrientation.getOrientation(scrollBar.getOrientation());
fSkin = fScrollBarSkinProvider.provideSkin(fOrientation);
super.installUI(c);
}
@Override
protected void uninstallComponents() {
if (incrButton != null)
scrollbar.remove(incrButton);
if (decrButton != null)
scrollbar.remove(decrButton);
}
@Override
protected void installComponents() {
// delegate to the ScrollBarSkin.
fSkin.installComponents(scrollbar);
}
@Override
protected void installListeners() {
super.installListeners();
// give the ScrollBarSkin the decrement and increment MouseListeners so that it may attach
// them to the appropriate components.
fSkin.installMouseListenersOnButtons(new CustomArrowButtonListener(-1), new CustomArrowButtonListener(1));
// repaint the scrollbar when the focus state of the parent window changes.
WindowUtils.installJComponentRepainterOnWindowFocusChanged(scrollbar);
}
@Override
public void layoutContainer(Container scrollbarContainer) {
if (isDragging) {
// do nothing.
} else if (isAllContentVisible(scrollbar)) {
// if all the content is visible, and thus no scrollbar is necssary, tell the
// ScrollBarSkin to layout only the track.
fSkin.layoutTrackOnly(scrollbar, fOrientation);
updateThumbBoundsFromScrollBarValue();
} else {
// tell the ScrollBarSkin to layout the entire scrollbar. once that's complete, update
// the bounds of the visible scroll thumb from the models value.
fSkin.layoutEverything(scrollbar, fOrientation);
updateThumbBoundsFromScrollBarValue();
}
}
@Override
protected Dimension getMinimumThumbSize() {
// delegate to the ScrollBarSkin.
return fSkin.getMinimumThumbSize();
}
@Override
public Dimension getPreferredSize(JComponent c) {
// delegate to the ScrollBarSkin.
return fSkin.getPreferredSize();
}
@Override
protected Rectangle getThumbBounds() {
// delegate to the ScrollBarSkin.
return fSkin.getScrollThumbBounds();
}
/**
* Convienence method that simply breaks apart the given Rectangle into its primatives and
* calls {@link #setThumbBounds(int, int, int, int)}.
*/
private void setThumbBounds(Rectangle thumbBounds) {
fSkin.setScrollThumbBounds(thumbBounds);
}
@Override
protected void setThumbBounds(int x, int y, int width, int height) {
setThumbBounds(new Rectangle(x, y, width, height));
}
@Override
protected Rectangle getTrackBounds() {
// delegate to the ScrollBarSkin.
return fSkin.getTrackBounds();
}
@Override
protected void paintIncreaseHighlight(Graphics g) {
// do nothing - not supported.
}
@Override
protected void paintDecreaseHighlight(Graphics g) {
// do nothing - not supported.
}
/**
* Sets the scroll thumb bounds based on the track size, the total size of viewable area and the
* amount of content that is currently visible.
*/
private void updateThumbBoundsFromScrollBarValue() {
// most of the below logic was lifted from BasicScrollBarUI. the logic here has been
// greatly simplified here through the use of the ScrollBarOrientation.
float min = scrollbar.getMinimum();
float extent = scrollbar.getVisibleAmount();
float range = scrollbar.getMaximum() - min;
float value = scrollbar.getValue();
int trackSize = fOrientation.getLength(fSkin.getTrackBounds().getSize());
int thumbLength = (int) (trackSize * (extent / range));
int minimumThumbLength = fOrientation.getLength(getMinimumThumbSize());
thumbLength = Math.max(thumbLength, minimumThumbLength);
float thumbRange = trackSize - thumbLength;
int thumbPosition = (int) (0.5f + (thumbRange * ((value - min) / (range - extent))));
// tell the ScrollBarSkin how big the scroll thumb should be.
fSkin.setScrollThumbBounds(
fOrientation.createBounds(scrollbar, thumbPosition, thumbLength));
}
/**
* Figures out where the scroll thumb should be based on the given MouseEvent and moves the
* thumb to that new location.
*/
private void updateThumbBoundsAndScrollBarValueFromMouseEvent(MouseEvent event, int offset) {
int mouseLocation = adjustMousePosition(event.getPoint(), offset);
updateThumbBoundsFromMouseLocation(mouseLocation);
updateScrollBarValueFromMouseLocation(mouseLocation);
}
/**
* Moves the visible scroll thumb to the given location.
*/
private void updateThumbBoundsFromMouseLocation(int mouseLocation) {
Dimension thumbSize = getThumbBounds().getSize();
Dimension trackSize = getTrackBounds().getSize();
// The mouseLocation is relative to the scrollbar but we need it
// relative to the track.
mouseLocation -= fOrientation.getPosition(getTrackBounds().getLocation());
// set the visible thumb bounds. this smoothly tracks where the user has the mouse.
// when they release the mouse, the actual scroll thumb position will be updated to
// reflect the exact scroll view window.
int thumbMaxPossiblePosition =
fOrientation.getLength(trackSize) - fOrientation.getLength(thumbSize);
int thumbPosition = Math.min(thumbMaxPossiblePosition, Math.max(0, mouseLocation));
// update the scroll thumb's position.
setThumbBounds(fOrientation.updateBoundsPosition(getThumbBounds(), thumbPosition));
}
/**
* Updaates the scrollbar model based on the given mouse location.
*/
private void updateScrollBarValueFromMouseLocation(int mouseLocation) {
// most of the below logic was lifted from BasicScrollBarUI. the logic here has been
// greatly simplified here through the use of the ScrollBarOrientation.
BoundedRangeModel model = scrollbar.getModel();
Rectangle thumbBounds = getThumbBounds();
Rectangle trackBounds = getTrackBounds();
// calculate what the value of the scrollbar should be.
int minimumPossibleThumbPosition = fOrientation.getPosition(trackBounds.getLocation());
int maximumPossibleThumbPosition = getMaximumPossibleThumbPosition(trackBounds, thumbBounds);
int actualThumbPosition = Math.min(maximumPossibleThumbPosition,
Math.max(minimumPossibleThumbPosition, mouseLocation));
// calculate the new value for the scroll bar (the top of the scroll thumb) based
// on the dragged location.
float valueMax = model.getMaximum() - model.getExtent();
float valueRange = valueMax - model.getMinimum();
float thumbValue = actualThumbPosition - minimumPossibleThumbPosition;
float thumbRange = maximumPossibleThumbPosition - minimumPossibleThumbPosition;
int value = (int) Math.ceil((thumbValue / thumbRange) * valueRange);
scrollbar.setValue(value + model.getMinimum());
}
/**
* Gets the maximum possible thumb position.
*/
private int getMaximumPossibleThumbPosition(Rectangle trackBounds, Rectangle thumbBounds) {
int trackStartPosition = fOrientation.getPosition(trackBounds.getLocation());
int trackLength = fOrientation.getLength(trackBounds.getSize());
int thumbLength = fOrientation.getLength(thumbBounds.getSize());
return trackStartPosition + trackLength - thumbLength;
}
private int adjustMousePosition(Point mousePoint, int offset) {
return fOrientation.getPosition(mousePoint) - offset;
}
/**
* True if the given point is before the start of the scroll thumb.
*/
private boolean isPointBeforeScrollThumb(Point point) {
int mousePosition = fOrientation.getPosition(point);
int thumbPosition = fOrientation.getPosition(getThumbBounds().getLocation());
return mousePosition < thumbPosition;
}
/**
* True if the given point is after the end of the scroll thumb.
*/
private boolean isPointAfterScrollThumb(Point point) {
int mousePosition = fOrientation.getPosition(point);
int thumbPosition = fOrientation.getPosition(getThumbBounds().getLocation())
+ fOrientation.getLength(getThumbBounds().getSize());
return mousePosition > thumbPosition;
}
private int getDirectionToMoveThumb(Point mousePoint) {
return isPointBeforeScrollThumb(mousePoint) ? -1 : 1;
}
@Override
protected TrackListener createTrackListener() {
return new SkinnableTrackListener();
}
/**
* True if the all the content that the scrollbar is scrolling for is currently visible.
*/
private static boolean isAllContentVisible(JScrollBar scrollBar) {
float extent = scrollBar.getVisibleAmount();
float range = scrollBar.getMaximum() - scrollBar.getMinimum();
return extent == 0.0 || extent / range == 1.0;
}
// SkinnableTrackListener implementation. ////////////////////////////////////////////////////
private class SkinnableTrackListener extends TrackListener {
private Point iMousePoint = new Point();
@Override
public void mousePressed(MouseEvent event) {
if (shouldHandleMousePressed(event)) {
doMousePressed(event);
}
}
@Override
public void mouseDragged(MouseEvent event) {
if (shouldHandleMouseDragged(event)) {
doMouseDragged(event);
}
}
private void startScrollTimerIfNecessary() {
if (isPointBeforeScrollThumb(iMousePoint) || isPointAfterScrollThumb(iMousePoint)) {
scrollTimer.start();
}
}
private void captureCurrentMousePosition(MouseEvent event) {
assert event.getSource() == scrollbar
: "The listener should be registered with the scrollbar for mouse events.";
currentMouseX = event.getX();
currentMouseY = event.getY();
// The mouse position is relative to the scrollbar but we need it
// relative to the track.
int trackPos = fOrientation.getPosition(getTrackBounds().getLocation());
if (fOrientation == ScrollBarOrientation.HORIZONTAL) {
currentMouseX -= trackPos;
} else {
currentMouseY -= trackPos;
}
iMousePoint.x = currentMouseX;
iMousePoint.y = currentMouseY;
}
private boolean isIgnorableMiddleMousePress(MouseEvent event) {
return SwingUtilities.isMiddleMouseButton(event) && !getSupportsAbsolutePositioning();
}
private boolean shouldHandleMousePressed(MouseEvent event) {
return !isIgnorableMiddleMousePress(event) && !SwingUtilities.isRightMouseButton(event)
&& scrollbar.isEnabled();
}
private boolean shouldHandleMouseDragged(MouseEvent event) {
return !isIgnorableMiddleMousePress(event) && !SwingUtilities.isRightMouseButton(event)
&& scrollbar.isEnabled() && !getThumbBounds().isEmpty();
}
private void doMousePressed(MouseEvent event) {
scrollbar.setValueIsAdjusting(true);
captureCurrentMousePosition(event);
if (getThumbBounds().contains(iMousePoint)) {
doMousePressedOnThumb();
} else if (getSupportsAbsolutePositioning() && SwingUtilities.isMiddleMouseButton(event)) {
doMiddleMouseButtonPressedOnTrack(event);
} else if (getTrackBounds().contains(iMousePoint)) {
doMousePressedOnTrack();
}
}
private void doMousePressedOnThumb() {
offset = fOrientation.getPosition(iMousePoint)
- fOrientation.getPosition(getThumbBounds().getLocation());
isDragging = true;
}
private void doMiddleMouseButtonPressedOnTrack(MouseEvent event) {
offset = fOrientation.getLength(getThumbBounds().getSize()) / 2;
isDragging = true;
updateThumbBoundsAndScrollBarValueFromMouseEvent(event, offset);
}
private void doMousePressedOnTrack() {
isDragging = false;
int direction = getDirectionToMoveThumb(iMousePoint);
scrollByBlock(direction);
scrollTimer.stop();
scrollListener.setDirection(direction);
scrollListener.setScrollByBlock(true);
startScrollTimerIfNecessary();
}
private void doMouseDragged(MouseEvent event) {
if (isDragging) {
updateThumbBoundsAndScrollBarValueFromMouseEvent(event, offset);
} else {
captureCurrentMousePosition(event);
startScrollTimerIfNecessary();
}
}
}
// IncrementButtonListener implementation. ////////////////////////////////////////////////////
protected class CustomArrowButtonListener extends ArrowButtonListener {
private final int iScrollDirection;
private CustomArrowButtonListener(int scrollDirection) {
iScrollDirection = scrollDirection;
}
public void mousePressed(MouseEvent e) {
if (scrollbar.isEnabled() && SwingUtilities.isLeftMouseButton(e)) {
scrollByUnit(iScrollDirection);
scrollTimer.stop();
scrollListener.setDirection(iScrollDirection);
scrollListener.setScrollByBlock(false);
scrollTimer.start();
}
}
public void mouseReleased(MouseEvent e) {
scrollTimer.stop();
scrollbar.setValueIsAdjusting(false);
}
}
// An interface for providing a ScrollBarSkin. ////////////////////////////////////////////////
public interface ScrollBarSkinProvider {
ScrollBarSkin provideSkin(ScrollBarOrientation orientation);
}
}