/*
* Copyright 2010-2015 Institut Pasteur.
*
* This file is part of Icy.
*
* Icy is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Icy 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with Icy. If not, see <http://www.gnu.org/licenses/>.
*/
package icy.gui.component.ui;
import icy.gui.component.RangeSlider;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Map;
import javax.swing.ButtonModel;
import javax.swing.DefaultButtonModel;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JSlider;
import javax.swing.SwingConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.pushingpixels.lafwidget.LafWidgetUtilities;
import org.pushingpixels.substance.api.ColorSchemeAssociationKind;
import org.pushingpixels.substance.api.ComponentState;
import org.pushingpixels.substance.api.SubstanceColorScheme;
import org.pushingpixels.substance.api.painter.border.SubstanceBorderPainter;
import org.pushingpixels.substance.api.painter.fill.ClassicFillPainter;
import org.pushingpixels.substance.api.painter.fill.SubstanceFillPainter;
import org.pushingpixels.substance.internal.animation.StateTransitionTracker;
import org.pushingpixels.substance.internal.animation.TransitionAwareUI;
import org.pushingpixels.substance.internal.painter.BackgroundPaintingUtils;
import org.pushingpixels.substance.internal.ui.SubstanceSliderUI;
import org.pushingpixels.substance.internal.utils.HashMapKey;
import org.pushingpixels.substance.internal.utils.RolloverControlListener;
import org.pushingpixels.substance.internal.utils.SubstanceColorSchemeUtilities;
import org.pushingpixels.substance.internal.utils.SubstanceCoreUtilities;
import org.pushingpixels.substance.internal.utils.SubstanceOutlineUtilities;
import org.pushingpixels.substance.internal.utils.SubstanceSizeUtils;
/**
* UI delegate for the RangeSlider component with Substance AndFeel.
* RangeSliderUI paints two thumbs, one for the lower value and one for the upper value.
*
* @author Stephane Dallongeville
*/
public class RangeSliderUI extends SubstanceSliderUI
{
/** Location and size of thumb for upper value. */
Rectangle upperThumbRect;
/** Indicator that determines whether upper thumb is selected. */
boolean upperThumbSelected;
/** Indicator that determines whether lower thumb is being dragged. */
transient boolean lowerDragging;
/** Indicator that determines whether upper thumb is being dragged. */
transient boolean upperDragging;
/**
* Surrogate button model for tracking the general slider transitions.
*/
ButtonModel sliderModel;
/**
* Surrogate button model for tracking the upper thumb transitions.
*/
ButtonModel upperThumbModel;
/**
* General slider transition tracker.
*/
protected StateTransitionTracker sliderStateTransitionTracker;
/**
* Upper thumb transition tracker.
*/
protected StateTransitionTracker upperThumbStateTransitionTracker;
/**
* Listener for general slider transition animations.
*/
protected RolloverControlListener sliderRolloverListener;
/**
* Listener for upper thumb transition animations.
*/
protected RolloverControlListener upperThumbRolloverListener;
/**
* Listener on property change events.
*/
protected PropertyChangeListener sliderPropertyChangeListener;
/**
* Needed to return correct transition tracker on thumb paint.
*/
private boolean paintingLowerThumb;
private boolean paintingUpperThumb;
/**
* Constructs a RangeSliderUI for the specified slider component.
*
* @param rangeSlider
* RangeSlider
*/
public RangeSliderUI(RangeSlider rangeSlider)
{
super(rangeSlider);
sliderModel = new DefaultButtonModel();
sliderModel.setArmed(false);
sliderModel.setSelected(false);
sliderModel.setPressed(false);
sliderModel.setRollover(false);
sliderModel.setEnabled(rangeSlider.isEnabled());
upperThumbModel = new DefaultButtonModel();
upperThumbModel.setArmed(false);
upperThumbModel.setSelected(false);
upperThumbModel.setPressed(false);
upperThumbModel.setRollover(false);
upperThumbModel.setEnabled(rangeSlider.isEnabled());
sliderStateTransitionTracker = new StateTransitionTracker(rangeSlider, sliderModel);
upperThumbStateTransitionTracker = new StateTransitionTracker(rangeSlider, upperThumbModel);
paintingLowerThumb = false;
paintingUpperThumb = false;
}
/**
* Installs this UI delegate on the specified component.
*/
@Override
public void installUI(JComponent c)
{
upperThumbRect = new Rectangle();
super.installUI(c);
}
@Override
protected void installListeners(JSlider slider)
{
super.installListeners(slider);
sliderRolloverListener = new RolloverControlListener(new TransitionAwareUI()
{
@Override
public boolean isInside(MouseEvent me)
{
final double x = me.getX();
final double y = me.getY();
return isInsideLowerThumbInternal(x, y) || isInsideUpperThumbInternal(x, y);
}
@Override
public StateTransitionTracker getTransitionTracker()
{
return sliderStateTransitionTracker;
}
}, sliderModel);
slider.addMouseListener(sliderRolloverListener);
slider.addMouseMotionListener(sliderRolloverListener);
upperThumbRolloverListener = new RolloverControlListener(new TransitionAwareUI()
{
@Override
public boolean isInside(MouseEvent me)
{
return isInsideUpperThumb(me.getX(), me.getY());
}
@Override
public StateTransitionTracker getTransitionTracker()
{
return upperThumbStateTransitionTracker;
}
}, upperThumbModel);
slider.addMouseListener(upperThumbRolloverListener);
slider.addMouseMotionListener(upperThumbRolloverListener);
sliderPropertyChangeListener = new PropertyChangeListener()
{
@Override
public void propertyChange(PropertyChangeEvent evt)
{
if ("enabled".equals(evt.getPropertyName()))
{
final boolean enabled = RangeSliderUI.this.slider.isEnabled();
sliderModel.setEnabled(enabled);
upperThumbModel.setEnabled(enabled);
}
}
};
slider.addPropertyChangeListener(sliderPropertyChangeListener);
sliderStateTransitionTracker.registerModelListeners();
sliderStateTransitionTracker.registerFocusListeners();
upperThumbStateTransitionTracker.registerModelListeners();
upperThumbStateTransitionTracker.registerFocusListeners();
}
@Override
protected void uninstallListeners(JSlider slider)
{
super.uninstallListeners(slider);
slider.removeMouseListener(sliderRolloverListener);
slider.removeMouseMotionListener(sliderRolloverListener);
sliderRolloverListener = null;
slider.removeMouseListener(upperThumbRolloverListener);
slider.removeMouseMotionListener(upperThumbRolloverListener);
upperThumbRolloverListener = null;
slider.removePropertyChangeListener(sliderPropertyChangeListener);
sliderPropertyChangeListener = null;
sliderStateTransitionTracker.unregisterModelListeners();
sliderStateTransitionTracker.unregisterFocusListeners();
upperThumbStateTransitionTracker.unregisterModelListeners();
upperThumbStateTransitionTracker.unregisterFocusListeners();
}
/**
* Creates a listener to handle track events in the specified slider.
*/
@Override
protected TrackListener createTrackListener(JSlider slider)
{
return new RangeTrackListener();
}
/**
* Creates a listener to handle change events in the specified slider.
*/
@Override
protected ChangeListener createChangeListener(JSlider slider)
{
return new ChangeHandler();
}
/**
* Updates the dimensions for both thumbs.
*/
@Override
protected void calculateThumbSize()
{
// Call superclass method for lower thumb size.
super.calculateThumbSize();
// Set upper thumb size.
upperThumbRect.setSize(thumbRect.width, thumbRect.height);
}
/**
* Updates the locations for both thumbs.
*/
@Override
protected void calculateThumbLocation()
{
// Call superclass method for lower thumb location.
super.calculateThumbLocation();
// Adjust upper value to snap to ticks if necessary.
if (slider.getSnapToTicks())
{
int upperValue = slider.getValue() + slider.getExtent();
int snappedValue = upperValue;
int majorTickSpacing = slider.getMajorTickSpacing();
int minorTickSpacing = slider.getMinorTickSpacing();
int tickSpacing = 0;
if (minorTickSpacing > 0)
{
tickSpacing = minorTickSpacing;
}
else if (majorTickSpacing > 0)
{
tickSpacing = majorTickSpacing;
}
if (tickSpacing != 0)
{
// If it's not on a tick, change the value
if ((upperValue - slider.getMinimum()) % tickSpacing != 0)
{
float temp = (float) (upperValue - slider.getMinimum()) / (float) tickSpacing;
int whichTick = Math.round(temp);
snappedValue = slider.getMinimum() + (whichTick * tickSpacing);
}
if (snappedValue != upperValue)
{
slider.setExtent(snappedValue - slider.getValue());
}
}
}
Rectangle trackRect = this.getPaintTrackRect();
if (slider.getOrientation() == SwingConstants.HORIZONTAL)
{
int valuePosition = xPositionForValue(slider.getValue() + slider.getExtent());
double centerY = trackRect.y + trackRect.height / 2.0;
upperThumbRect.y = (int) (centerY - upperThumbRect.height / 2.0) + 1;
upperThumbRect.x = valuePosition - upperThumbRect.width / 2;
}
else
{
int valuePosition = yPositionForValue(slider.getValue() + slider.getExtent());
double centerX = trackRect.x + trackRect.width / 2.0;
upperThumbRect.x = (int) (centerX - upperThumbRect.width / 2.0) + 1;
upperThumbRect.y = valuePosition - (upperThumbRect.height / 2);
}
}
@Override
public boolean isInside(MouseEvent me)
{
return isInsideLowerThumb(me.getX(), me.getY());
}
public boolean isInsideLowerThumbInternal(double x, double y)
{
final Rectangle thumbB = this.thumbRect;
return thumbB != null && thumbB.contains(x, y);
}
public boolean isInsideLowerThumb(double x, double y)
{
// inside lower ?
if (isInsideLowerThumbInternal(x, y))
{
// also inside upper ?
if (isInsideUpperThumbInternal(x, y))
{
final double dl = Point2D.distance(thumbRect.getCenterX(), thumbRect.getCenterY(), x, y);
final double du = Point2D.distance(upperThumbRect.getCenterX(), upperThumbRect.getCenterY(), x, y);
return (dl < du);
}
return true;
}
return false;
}
public boolean isInsideUpperThumbInternal(double x, double y)
{
final Rectangle upperThumbR = upperThumbRect;
return (upperThumbR != null) && upperThumbR.contains(x, y);
}
public boolean isInsideUpperThumb(double x, double y)
{
// inside lower ?
if (isInsideUpperThumbInternal(x, y))
{
// also inside upper ?
if (isInsideLowerThumbInternal(x, y))
{
// find closest one
final double dl = Point2D.distance(thumbRect.getCenterX(), thumbRect.getCenterY(), x, y);
final double du = Point2D.distance(upperThumbRect.getCenterX(), upperThumbRect.getCenterY(), x, y);
return (du <= dl);
}
return true;
}
return false;
}
@Override
public StateTransitionTracker getTransitionTracker()
{
if (paintingLowerThumb)
return super.getTransitionTracker();
if (paintingUpperThumb)
return upperThumbStateTransitionTracker;
return sliderStateTransitionTracker;
}
/**
* Returns the rectangle of track for painting.
*
* @return The rectangle of track for painting.
*/
private Rectangle getPaintTrackRect()
{
int trackLeft = 0;
int trackRight;
int trackTop = 0;
int trackBottom;
int trackWidth = this.getTrackWidth();
if (this.slider.getOrientation() == SwingConstants.HORIZONTAL)
{
trackTop = 3 + this.insetCache.top + 2 * this.focusInsets.top;
trackBottom = trackTop + trackWidth - 1;
trackRight = this.trackRect.width;
return new Rectangle(this.trackRect.x + trackLeft, trackTop, trackRight - trackLeft, trackBottom - trackTop);
}
if (this.slider.getPaintLabels() || this.slider.getPaintTicks())
{
if (this.slider.getComponentOrientation().isLeftToRight())
{
trackLeft = trackRect.x + this.insetCache.left + this.focusInsets.left;
trackRight = trackLeft + trackWidth - 1;
}
else
{
trackRight = trackRect.x + trackRect.width - this.insetCache.right - this.focusInsets.right;
trackLeft = trackRight - trackWidth - 1;
}
}
else
{
// horizontally center the track
if (this.slider.getComponentOrientation().isLeftToRight())
{
trackLeft = (this.insetCache.left + this.focusInsets.left + this.slider.getWidth()
- this.insetCache.right - this.focusInsets.right)
/ 2 - trackWidth / 2;
trackRight = trackLeft + trackWidth - 1;
}
else
{
trackRight = (this.insetCache.left + this.focusInsets.left + this.slider.getWidth()
- this.insetCache.right - this.focusInsets.right)
/ 2 + trackWidth / 2;
trackLeft = trackRight - trackWidth - 1;
}
}
trackBottom = this.trackRect.height - 1;
return new Rectangle(trackLeft, this.trackRect.y + trackTop, trackRight - trackLeft, trackBottom - trackTop);
}
@Override
public void paint(Graphics g, final JComponent c)
{
Graphics2D graphics = (Graphics2D) g.create();
ComponentState currState = ComponentState.getState(sliderModel, slider);
float alpha = SubstanceColorSchemeUtilities.getAlpha(slider, currState);
BackgroundPaintingUtils.updateIfOpaque(graphics, c);
recalculateIfInsetsChanged();
recalculateIfOrientationChanged();
final Rectangle clip = graphics.getClipBounds();
if (!clip.intersects(trackRect) && slider.getPaintTrack())
calculateGeometry();
graphics.setComposite(LafWidgetUtilities.getAlphaComposite(this.slider, alpha, g));
if (slider.getPaintTrack() && clip.intersects(trackRect))
{
paintTrack(graphics);
}
if (slider.getPaintTicks() && clip.intersects(tickRect))
{
paintTicks(graphics);
}
// don't paint focus as component is not focusable
// paintFocus(graphics);
if (clip.intersects(thumbRect))
{
paintLowerThumb(graphics);
}
if (clip.intersects(upperThumbRect))
{
paintUpperThumb(graphics);
}
graphics.setComposite(LafWidgetUtilities.getAlphaComposite(this.slider, 1.0f, g));
if (slider.getPaintLabels() && clip.intersects(labelRect))
{
paintLabels(graphics);
}
graphics.dispose();
}
public void paintLowerThumb(Graphics g)
{
paintingLowerThumb = true;
// default implementation
paintThumb(g);
paintingLowerThumb = false;
}
public void paintUpperThumb(Graphics g)
{
paintingUpperThumb = true;
final Graphics2D graphics = (Graphics2D) g.create();
final Rectangle knobBounds = upperThumbRect;
graphics.translate(knobBounds.x, knobBounds.y);
final Icon icon = getIcon();
if (slider.getOrientation() == SwingConstants.HORIZONTAL)
{
if (icon != null)
icon.paintIcon(this.slider, graphics, -1, 0);
}
else
{
if (slider.getComponentOrientation().isLeftToRight())
{
if (icon != null)
icon.paintIcon(this.slider, graphics, 0, -1);
}
else
{
if (icon != null)
icon.paintIcon(this.slider, graphics, 0, 1);
}
}
graphics.dispose();
paintingUpperThumb = false;
}
@Override
public void paintTrack(Graphics g)
{
Graphics2D graphics = (Graphics2D) g.create();
boolean drawInverted = drawInverted();
Rectangle paintRect = getPaintTrackRect();
// Width and height of the painting rectangle.
int width = paintRect.width;
int height = paintRect.height;
if (this.slider.getOrientation() == SwingConstants.VERTICAL)
{
// apply rotation / translate transformation on vertical
// slider tracks
int temp = width;
// noinspection SuspiciousNameCombination
width = height;
height = temp;
AffineTransform at = graphics.getTransform();
at.translate(paintRect.x, width + paintRect.y);
at.rotate(-Math.PI / 2);
graphics.setTransform(at);
}
else
{
graphics.translate(paintRect.x, paintRect.y);
}
StateTransitionTracker.ModelStateInfo modelStateInfo = sliderStateTransitionTracker.getModelStateInfo();
SubstanceColorScheme trackSchemeUnselected = SubstanceColorSchemeUtilities.getColorScheme(this.slider,
slider.isEnabled() ? ComponentState.ENABLED : ComponentState.DISABLED_UNSELECTED);
SubstanceColorScheme trackBorderSchemeUnselected = SubstanceColorSchemeUtilities.getColorScheme(this.slider,
ColorSchemeAssociationKind.BORDER, this.slider.isEnabled() ? ComponentState.ENABLED
: ComponentState.DISABLED_UNSELECTED);
paintSliderTrack(graphics, drawInverted, trackSchemeUnselected, trackBorderSchemeUnselected, width, height);
Map<ComponentState, StateTransitionTracker.StateContributionInfo> activeStates = modelStateInfo
.getStateContributionMap();
for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> activeEntry : activeStates
.entrySet())
{
ComponentState activeState = activeEntry.getKey();
if (!activeState.isActive())
continue;
float contribution = activeEntry.getValue().getContribution();
if (contribution == 0.0f)
continue;
graphics.setComposite(LafWidgetUtilities.getAlphaComposite(slider, contribution, g));
SubstanceColorScheme activeFillScheme = SubstanceColorSchemeUtilities.getColorScheme(this.slider,
activeState);
SubstanceColorScheme activeBorderScheme = SubstanceColorSchemeUtilities.getColorScheme(this.slider,
ColorSchemeAssociationKind.BORDER, activeState);
paintSliderTrackSelected(graphics, paintRect, activeFillScheme, activeBorderScheme, width, height);
}
graphics.dispose();
}
/**
* Paints the slider track.
*
* @param graphics
* Graphics.
* @param drawInverted
* Indicates whether the value-range shown for the slider is
* reversed.
* @param fillColorScheme
* Fill color scheme.
* @param borderScheme
* Border color scheme.
* @param width
* Track width.
* @param height
* Track height.
*/
private void paintSliderTrack(Graphics2D graphics, boolean drawInverted, SubstanceColorScheme fillColorScheme,
SubstanceColorScheme borderScheme, int width, int height)
{
Graphics2D g2d = (Graphics2D) graphics.create();
SubstanceFillPainter fillPainter = ClassicFillPainter.INSTANCE;
SubstanceBorderPainter borderPainter = SubstanceCoreUtilities.getBorderPainter(this.slider);
int componentFontSize = SubstanceSizeUtils.getComponentFontSize(this.slider);
int borderDelta = (int) Math.floor(SubstanceSizeUtils.getBorderStrokeWidth(componentFontSize) / 2.0);
float radius = SubstanceSizeUtils.getClassicButtonCornerRadius(componentFontSize) / 2.0f;
int borderThickness = (int) SubstanceSizeUtils.getBorderStrokeWidth(componentFontSize);
HashMapKey key = SubstanceCoreUtilities.getHashKey(width, height, radius, borderDelta, borderThickness,
fillColorScheme.getDisplayName(), borderScheme.getDisplayName());
BufferedImage trackImage = trackCache.get(key);
if (trackImage == null)
{
trackImage = SubstanceCoreUtilities.getBlankImage(width + 1, height + 1);
Graphics2D cacheGraphics = trackImage.createGraphics();
Shape contour = SubstanceOutlineUtilities.getBaseOutline(width + 1, height + 1, radius, null, borderDelta);
fillPainter.paintContourBackground(cacheGraphics, slider, width, height, contour, false, fillColorScheme,
false);
GeneralPath contourInner = SubstanceOutlineUtilities.getBaseOutline(width + 1, height + 1, radius
- borderThickness, null, borderThickness + borderDelta);
borderPainter
.paintBorder(cacheGraphics, slider, width + 1, height + 1, contour, contourInner, borderScheme);
trackCache.put(key, trackImage);
cacheGraphics.dispose();
}
g2d.drawImage(trackImage, 0, 0, null);
g2d.dispose();
}
/**
* Paints the selected part of the slider track.
*
* @param graphics
* Graphics.
* @param drawInverted
* Indicates whether the value-range shown for the slider is
* reversed.
* @param paintRect
* Selected portion.
* @param fillScheme
* Fill color scheme.
* @param borderScheme
* Border color scheme.
* @param width
* Track width.
* @param height
* Track height.
*/
private void paintSliderTrackSelected(Graphics2D graphics, Rectangle paintRect, SubstanceColorScheme fillScheme,
SubstanceColorScheme borderScheme, int width, int height)
{
Graphics2D g2d = (Graphics2D) graphics.create();
Insets insets = this.slider.getInsets();
insets.top /= 2;
insets.left /= 2;
insets.bottom /= 2;
insets.right /= 2;
SubstanceFillPainter fillPainter = SubstanceCoreUtilities.getFillPainter(this.slider);
SubstanceBorderPainter borderPainter = SubstanceCoreUtilities.getBorderPainter(this.slider);
float radius = SubstanceSizeUtils.getClassicButtonCornerRadius(SubstanceSizeUtils.getComponentFontSize(slider)) / 2.0f;
int borderDelta = (int) Math.floor(SubstanceSizeUtils.getBorderStrokeWidth(SubstanceSizeUtils
.getComponentFontSize(slider)) / 2.0);
// fill selected portion
if (this.slider.isEnabled())
{
if (this.slider.getOrientation() == SwingConstants.HORIZONTAL)
{
int ltPos = thumbRect.x + (this.thumbRect.width / 2) - paintRect.x;
int utPos = upperThumbRect.x + (this.upperThumbRect.width / 2) - paintRect.x;
int fillMinX;
int fillMaxX;
if (ltPos < utPos)
{
fillMinX = ltPos;
fillMaxX = utPos;
}
else
{
fillMinX = utPos;
fillMaxX = ltPos;
}
int fillWidth = fillMaxX - fillMinX;
int fillHeight = height + 1;
if ((fillWidth > 0) && (fillHeight > 0))
{
Shape contour = SubstanceOutlineUtilities.getBaseOutline(fillWidth, fillHeight, radius, null,
borderDelta);
g2d.translate(fillMinX, 0);
fillPainter.paintContourBackground(g2d, this.slider, fillWidth, fillHeight, contour, false,
fillScheme, false);
borderPainter.paintBorder(g2d, this.slider, fillWidth, fillHeight, contour, null, borderScheme);
}
}
else
{
int ltPos = thumbRect.y + (this.thumbRect.height / 2) - paintRect.y;
int utPos = upperThumbRect.y + (this.upperThumbRect.height / 2) - paintRect.y;
int fillMin;
int fillMax;
if (ltPos < utPos)
{
fillMin = ltPos;
fillMax = utPos;
}
else
{
fillMin = utPos;
fillMax = ltPos;
}
// if (this.drawInverted())
// {
// fillMin = 0;
// fillMax = middleOfThumb;
// // fix for issue 368 - inverted vertical sliders
// g2d.translate(width + 2 - middleOfThumb, 0);
// }
// else
// {
// fillMin = middleOfThumb;
// fillMax = width + 1;
// }
int fillWidth = fillMax - fillMin;
int fillHeight = height + 1;
if ((fillWidth > 0) && (fillHeight > 0))
{
Shape contour = SubstanceOutlineUtilities.getBaseOutline(fillWidth, fillHeight, radius, null,
borderDelta);
g2d.translate(paintRect.height - fillMax, 0);
fillPainter.paintContourBackground(g2d, this.slider, fillWidth, fillHeight, contour, false,
fillScheme, false);
borderPainter.paintBorder(g2d, this.slider, fillWidth, fillHeight, contour, null, borderScheme);
}
}
}
g2d.dispose();
}
/**
* Sets the location of the upper thumb, and repaints the slider. This is
* called when the upper thumb is dragged to repaint the slider. The
* <code>setThumbLocation()</code> method performs the same task for the
* lower thumb.
*/
void setUpperThumbLocation(int x, int y)
{
upperThumbRect.setLocation(x, y);
slider.repaint();
}
/**
* Moves the selected thumb in the specified direction by a block increment.
* This method is called when the user presses the Page Up or Down keys.
*/
@Override
public void scrollByBlock(int direction)
{
synchronized (slider)
{
int blockIncrement = (slider.getMaximum() - slider.getMinimum()) / 10;
if (blockIncrement <= 0 && slider.getMaximum() > slider.getMinimum())
{
blockIncrement = 1;
}
int delta = blockIncrement * ((direction > 0) ? POSITIVE_SCROLL : NEGATIVE_SCROLL);
if (upperThumbSelected)
{
int oldValue = ((RangeSlider) slider).getUpperValue();
((RangeSlider) slider).setUpperValue(oldValue + delta);
}
else
{
int oldValue = slider.getValue();
slider.setValue(oldValue + delta);
}
}
}
/**
* Moves the selected thumb in the specified direction by a unit increment.
* This method is called when the user presses one of the arrow keys.
*/
@Override
public void scrollByUnit(int direction)
{
synchronized (slider)
{
int delta = 1 * ((direction > 0) ? POSITIVE_SCROLL : NEGATIVE_SCROLL);
if (upperThumbSelected)
{
int oldValue = ((RangeSlider) slider).getUpperValue();
((RangeSlider) slider).setUpperValue(oldValue + delta);
}
else
{
int oldValue = slider.getValue();
slider.setValue(oldValue + delta);
}
}
}
/**
* Listener to handle model change events. This calculates the thumb
* locations and repaints the slider if the value change is not caused by
* dragging a thumb.
*/
public class ChangeHandler implements ChangeListener
{
@Override
public void stateChanged(ChangeEvent arg0)
{
if (!lowerDragging && !upperDragging)
{
calculateThumbLocation();
slider.repaint();
}
}
}
/**
* Listener to handle mouse movements in the slider track.
*/
public class RangeTrackListener extends TrackListener
{
@Override
public void mousePressed(MouseEvent e)
{
if (!slider.isEnabled())
return;
currentMouseX = e.getX();
currentMouseY = e.getY();
if (slider.isRequestFocusEnabled())
slider.requestFocus();
// Determine which thumb is pressed. If the upper thumb is
// selected (last one dragged), then check its position first;
// otherwise check the position of the lower thumb first.
boolean lowerPressed = false;
boolean upperPressed = false;
if (isInsideLowerThumb(currentMouseX, currentMouseY))
lowerPressed = true;
else if (isInsideUpperThumb(currentMouseX, currentMouseY))
upperPressed = true;
// Handle lower thumb pressed.
if (lowerPressed)
{
switch (slider.getOrientation())
{
case SwingConstants.VERTICAL:
offset = currentMouseY - thumbRect.y;
break;
case SwingConstants.HORIZONTAL:
offset = currentMouseX - thumbRect.x;
break;
}
upperThumbSelected = false;
lowerDragging = true;
return;
}
lowerDragging = false;
// Handle upper thumb pressed.
if (upperPressed)
{
switch (slider.getOrientation())
{
case SwingConstants.VERTICAL:
offset = currentMouseY - upperThumbRect.y;
break;
case SwingConstants.HORIZONTAL:
offset = currentMouseX - upperThumbRect.x;
break;
}
upperThumbSelected = true;
upperDragging = true;
return;
}
upperDragging = false;
}
@Override
public void mouseReleased(MouseEvent e)
{
lowerDragging = false;
upperDragging = false;
slider.setValueIsAdjusting(false);
super.mouseReleased(e);
}
@Override
public void mouseDragged(MouseEvent e)
{
if (!slider.isEnabled())
{
return;
}
currentMouseX = e.getX();
currentMouseY = e.getY();
if (lowerDragging)
{
slider.setValueIsAdjusting(true);
moveLowerThumb();
}
else if (upperDragging)
{
slider.setValueIsAdjusting(true);
moveUpperThumb();
}
}
@Override
public boolean shouldScroll(int direction)
{
return false;
}
/**
* Moves the location of the lower thumb, and sets its corresponding
* value in the slider.
*/
private void moveLowerThumb()
{
int thumbMiddle = 0;
switch (slider.getOrientation())
{
case SwingConstants.VERTICAL:
int halfThumbHeight = thumbRect.height / 2;
int thumbTop = currentMouseY - offset;
int trackTop = trackRect.y;
int trackBottom = trackRect.y + (trackRect.height - 1);
int vMax = yPositionForValue(slider.getValue() + slider.getExtent());
// Apply bounds to thumb position.
if (drawInverted())
{
trackBottom = vMax;
}
else
{
trackTop = vMax;
}
thumbTop = Math.max(thumbTop, trackTop - halfThumbHeight);
thumbTop = Math.min(thumbTop, trackBottom - halfThumbHeight);
setThumbLocation(thumbRect.x, thumbTop);
// Update slider value.
thumbMiddle = thumbTop + halfThumbHeight;
slider.setValue(valueForYPosition(thumbMiddle));
break;
case SwingConstants.HORIZONTAL:
int halfThumbWidth = thumbRect.width / 2;
int thumbLeft = currentMouseX - offset;
int trackLeft = trackRect.x;
int trackRight = trackRect.x + (trackRect.width - 1);
int hMax = xPositionForValue(slider.getValue() + slider.getExtent());
// Apply bounds to thumb position.
if (drawInverted())
{
trackLeft = hMax;
}
else
{
trackRight = hMax;
}
thumbLeft = Math.max(thumbLeft, trackLeft - halfThumbWidth);
thumbLeft = Math.min(thumbLeft, trackRight - halfThumbWidth);
setThumbLocation(thumbLeft, thumbRect.y);
// Update slider value.
thumbMiddle = thumbLeft + halfThumbWidth;
slider.setValue(valueForXPosition(thumbMiddle));
break;
default:
return;
}
}
/**
* Moves the location of the upper thumb, and sets its corresponding
* value in the slider.
*/
private void moveUpperThumb()
{
int thumbMiddle = 0;
switch (slider.getOrientation())
{
case SwingConstants.VERTICAL:
int halfThumbHeight = thumbRect.height / 2;
int thumbTop = currentMouseY - offset;
int trackTop = trackRect.y;
int trackBottom = trackRect.y + (trackRect.height - 1);
int vMin = yPositionForValue(slider.getValue());
// Apply bounds to thumb position.
if (drawInverted())
{
trackTop = vMin;
}
else
{
trackBottom = vMin;
}
thumbTop = Math.max(thumbTop, trackTop - halfThumbHeight);
thumbTop = Math.min(thumbTop, trackBottom - halfThumbHeight);
setUpperThumbLocation(thumbRect.x, thumbTop);
// Update slider extent.
thumbMiddle = thumbTop + halfThumbHeight;
slider.setExtent(valueForYPosition(thumbMiddle) - slider.getValue());
break;
case SwingConstants.HORIZONTAL:
int halfThumbWidth = thumbRect.width / 2;
int thumbLeft = currentMouseX - offset;
int trackLeft = trackRect.x;
int trackRight = trackRect.x + (trackRect.width - 1);
int hMin = xPositionForValue(slider.getValue());
// Apply bounds to thumb position.
if (drawInverted())
{
trackRight = hMin;
}
else
{
trackLeft = hMin;
}
thumbLeft = Math.max(thumbLeft, trackLeft - halfThumbWidth);
thumbLeft = Math.min(thumbLeft, trackRight - halfThumbWidth);
setUpperThumbLocation(thumbLeft, thumbRect.y);
// Update slider extent.
thumbMiddle = thumbLeft + halfThumbWidth;
slider.setExtent(valueForXPosition(thumbMiddle) - slider.getValue());
break;
default:
return;
}
}
}
}