/* Copyright (C) 2005-2011 Fabio Riccardi */
package com.lightcrafts.ui.swing;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.lightcrafts.platform.Platform;
import com.lightcrafts.ui.LightZoneSkin;
import com.lightcrafts.ui.toolkit.ShadowFactory;
/**
* A <code>RangeSelector</code> is like a {@link JSlider} except that it has
* two knobs.
*
* @author Paul J. Lucas [paul@lightcrafts.com]
*/
public class RangeSelector extends JComponent {
/**
* A <code>Track</code> is used to paint the track of a
* {@link RangeSelector}.
*/
public interface Track {
/**
* Paints the track of a {@link RangeSelector}.
*
* @param s The {@link RangeSelector} to paint the track of.
* @param r The {@link Rectangle} to paint within.
* @param g The graphics context to use.
*/
void paintTrack( RangeSelector s, Rectangle r, Graphics2D g );
}
/**
* Constructs a <code>RangeSelector</code>.
*/
public RangeSelector() {
this( new RangeSelectorModel(), false );
}
/**
* Constructs a <code>RangeSelector</code>.
*
* @param minimumThumbValue The minimum thumb value.
* @param maximumThumbValue The maximum thumb value.
*/
public RangeSelector( int minimumThumbValue, int maximumThumbValue ) {
this(
minimumThumbValue, maximumThumbValue,
minimumThumbValue, minimumThumbValue,
maximumThumbValue, maximumThumbValue
);
}
/**
* Constructs a <code>RangeSelector</code>.
*
* @param minimumThumbValue The minimum thumb value.
* @param maximumThumbValue The maximum thumb value.
* @param lowerThumbValue The lower thumb value.
* @param lowerFeatheringValue The lower thumb feathering value.
* @param upperThumbValue The upper thumb value.
* @param upperThumbFeatheringValue The upper thumb feathering value.
*/
public RangeSelector( int minimumThumbValue, int maximumThumbValue,
int lowerThumbValue, int lowerFeatheringValue,
int upperThumbValue, int upperThumbFeatheringValue ) {
this(
new RangeSelectorModel(
minimumThumbValue, maximumThumbValue,
lowerThumbValue, lowerFeatheringValue,
upperThumbValue, upperThumbFeatheringValue,
0, Integer.MAX_VALUE, 0
),
false
);
}
/**
* Constructs a <code>RangeSelector</code>.
*
* @param minimumThumbValue The minimum thumb value.
* @param maximumThumbValue The maximum thumb value.
* @param lowerThumbValue The lower thumb value.
* @param lowerFeatheringValue The lower thumb feathering value.
* @param upperThumbValue The upper thumb value.
* @param upperThumbFeatheringValue The upper thumb feathering value.
*/
public RangeSelector( int minimumThumbValue, int maximumThumbValue,
int lowerThumbValue, int lowerFeatheringValue,
int upperThumbValue, int upperThumbFeatheringValue,
int minimumTrackValue, int maximumTrackValue,
int trackValue, boolean trackValueWraps ) {
this(
new RangeSelectorModel(
minimumThumbValue, maximumThumbValue,
lowerThumbValue, lowerFeatheringValue,
upperThumbValue, upperThumbFeatheringValue,
minimumTrackValue, maximumTrackValue, trackValue
),
trackValueWraps
);
}
/**
* Constructs a <code>RangeSelector</code>.
*
* @param model The {@link RangeSelectorModel} to use.
* @param trackValueWraps If <code>true</code>, the track value is allowed
* to wrap around.
*/
@SuppressWarnings({"OverridableMethodCallInConstructor"})
public RangeSelector( RangeSelectorModel model, boolean trackValueWraps ) {
m_model = model;
m_trackValueWraps = trackValueWraps;
addComponentListener(
new ComponentAdapter() {
public void componentResized( ComponentEvent e ) {
stateChanged();
}
}
);
setFocusable( false );
final LocalMouseListener listener = new LocalMouseListener();
addMouseListener( listener );
addMouseMotionListener( listener );
}
/**
* Adds a {@link ChangeListener} to this component.
*
* @param listener The {@link ChangeListener} to add.
* @see #removeChangeListener(ChangeListener)
*/
public void addChangeListener( ChangeListener listener ) {
listenerList.add( ChangeListener.class, listener );
}
/**
* Gets the track value.
*
* @return Returns said value.
* @see #setTrackValue(int)
*/
public int getTrackValue() {
return m_model.getTrackValue();
}
/**
* Gets the track modulo value.
*
* @return Returns said value.
* @see #getTrackValue()
*/
public boolean getTrackValueWraps() {
return m_trackValueWraps;
}
/**
* Returns an array of all the {@link ChangeListener}s added to this
* component with {@link #addChangeListener(ChangeListener)}.
*
* @return Returns all of the {@link ChangeListener}s added or an empty
* array if no listeners have been added.
*/
public ChangeListener[] getChangeListeners() {
return listenerList.getListeners( ChangeListener.class );
}
/**
* Gets the lower thumb feathering value.
*
* @return Returns said value.
* @see #getLowerThumbFeatheringValueX()
* @see #getUpperThumbFeatheringValue()
* @see #getLowerThumbValue()
*/
public int getLowerThumbFeatheringValue() {
return m_model.getLowerThumbFeatheringValue();
}
/**
* Gets the X coordinate of the lower thumb feathering value.
*
* @return Returns said coordinate.
* @see #getLowerThumbFeatheringValue()
* @see #getUpperThumbFeatheringValueX()
*/
public int getLowerThumbFeatheringValueX() {
return m_polygons[ LOWER_FEATHERING_THUMB ].xpoints[0];
}
/**
* Gets the lower thumb value.
*
* @return Returns said value.
* @see #getLowerThumbFeatheringValueX()
* @see #getUpperThumbValue()
*/
public int getLowerThumbValue() {
return m_model.getLowerThumbValue();
}
/**
* Gets the X coordinate of the lower thumb value.
*
* @return Returns said coordinate.
* @see #getLowerThumbFeatheringValueX()
* @see #getLowerThumbValue()
* @see #getUpperThumbValueX()
*/
public int getLowerThumbValueX() {
return m_polygons[ LOWER_THUMB ].xpoints[0];
}
/**
* Gets the maximum thumb value.
*
* @return Returns said value.
* @see #getMinimumThumbValue()
*/
public int getMaximumThumbValue() {
return m_model.getMaximumThumbValue();
}
/**
* {@inheritDoc}
*/
public Dimension getMaximumSize() {
final Insets in = getInsets();
return new Dimension(
Integer.MAX_VALUE,
in.top + in.bottom + Math.max( ARROW_HEIGHT, THUMB_HEIGHT )
);
}
/**
* Gets the maximum track value.
*
* @return Returns said value.
* @see #getMinimumTrackValue()
*/
public int getMaximumTrackValue() {
return m_model.getMaximumTrackValue();
}
/**
* Gets the minimum thumb value.
*
* @return Returns said value.
* @see #getMaximumThumbValue()
*/
public int getMinimumThumbValue() {
return m_model.getMinimumThumbValue();
}
/**
* {@inheritDoc}
*/
public Dimension getMinimumSize() {
final Insets in = getInsets();
return new Dimension(
100,
in.top + in.bottom + Math.max( ARROW_HEIGHT, THUMB_HEIGHT )
);
}
/**
* Gets the minimum track value.
*
* @return Returns said value.
* @see #getMaximumTrackValue()
*/
public int getMinimumTrackValue() {
return m_model.getMinimumTrackValue();
}
/**
* Gets the {@link RangeSelectorModel} is use.
*
* @return Returns said model.
* @see #setModel(RangeSelectorModel)
*/
public RangeSelectorModel getModel() {
return m_model;
}
/**
* {@inheritDoc}
*/
public Dimension getPreferredSize() {
final int modelWidth =
m_model.getMaximumThumbValue() - m_model.getMinimumThumbValue();
final Insets in = getInsets();
return new Dimension(
modelWidth + in.left + in.right + ARROW_WIDTH * 2 + THUMB_WIDTH,
in.top + in.bottom + Math.max( ARROW_HEIGHT, THUMB_HEIGHT )
);
}
/**
* Gets the current {@link Track}.
*
* @return Returns said {@link Track}.
* @see #setTrack(Track)
*/
public Track getTrack() {
return m_track;
}
/**
* Gets the upper thumb feathering value.
*
* @return Returns said value.
* @see #getLowerThumbFeatheringValue()
* @see #getUpperThumbFeatheringValueX()
*/
public int getUpperThumbFeatheringValue() {
return m_model.getUpperThumbFeatheringValue();
}
/**
* Gets the X coordinate of the upper thumb feathering value.
*
* @return Returns said coordinate.
* @see #getLowerThumbValueX()
* @see #getUpperThumbFeatheringValue()
* @see #getUpperThumbValueX()
*/
public int getUpperThumbFeatheringValueX() {
return m_polygons[ UPPER_FEATHERING_THUMB ].xpoints[0];
}
/**
* Gets the upper thumb value.
*
* @return Returns said value.
* @see #getLowerThumbValue()
* @see #getUpperThumbValueX()
*/
public int getUpperThumbValue() {
return m_model.getUpperThumbValue();
}
/**
* Gets the X coordinate of the upper thumb value.
*
* @return Returns said coordinate.
* @see #getLowerThumbValueX()
* @see #getUpperThumbFeatheringValueX()
* @see #getUpperThumbValue()
*/
public int getUpperThumbValueX() {
return m_polygons[ UPPER_THUMB ].xpoints[0];
}
/**
* Removes a {@link ChangeListener} from this component.
*
* @param listener The {@link ChangeListener} to remove.
* @see #addChangeListener(ChangeListener)
*/
public void removeChangeListener( ChangeListener listener ) {
listenerList.remove( ChangeListener.class, listener );
}
/**
* Sets the lower thumb feathering value. The following constraints must
* always hold:
* <blockquote>
* min <= LTFV <= LTV <= UTV <= UTFV <= max
* </blockquote>
* @param newValue The new lower feathering value.
* @throws IllegalArgumentException if any constraint is violated.
* @see #getLowerThumbFeatheringValue()
* @see #setLowerThumbValue(int)
*/
public void setLowerThumbFeatheringValue( int newValue ) {
if ( m_model.setLowerFeatheringValue( newValue ) )
fireStateChanged();
}
/**
* Sets the lower thumb value. The following constraints must always hold:
* <blockquote>
* min <= LTFV <= LTV <= UTV <= UTFV <= max
* </blockquote>
* @param newValue The new lower thumb value.
* @throws IllegalArgumentException if any constraint is violated.
* @see #getLowerThumbValue()
* @see #setLowerThumbFeatheringValue(int)
*/
public void setLowerThumbValue( int newValue ) {
if ( m_model.setLowerThumbValue( newValue ) )
fireStateChanged();
}
/**
* Sets the maximum thumb value. The following constraints must always
* hold:
* <blockquote>
* min <= LTFV <= LTV <= UTV <= UTFV <= max
* </blockquote>
*
* @param newValue The new maximum thumb value.
* @throws IllegalArgumentException if any constraint is violated.
* @see #setMinimumThumbValue(int)
*/
public void setMaximumThumbValue( int newValue ) {
if ( m_model.setMaximumThumbValue( newValue ) )
fireStateChanged();
}
/**
* Sets the minimum thumb value. The following constraints must always
* hold:
* <blockquote>
* min <= LTFV <= LTV <= UTV <= UTFV <= max
* </blockquote>
*
* @param newValue The new minimum thumb value.
* @throws IllegalArgumentException if any constraint is violated.
* @see #setMaximumThumbValue(int)
*/
public void setMinimumThumbValue( int newValue ) {
if ( m_model.setMinimumThumbValue( newValue ) )
fireStateChanged();
}
/**
* Sets the model to use.
*
* @param newModel The new {@link RangeSelectorModel}.
* @see #getModel()
*/
public void setModel( RangeSelectorModel newModel ) {
if ( newModel != m_model ) {
m_model = newModel;
fireStateChanged();
}
}
/**
* Sets the various properties of the selector. The following constraints
* must always hold:
* <blockquote>
* min <= LTFV <= LTV <= UTV <= UTFV <= max
* </blockquote>
*
* @param minimumThumbValue The minimum thumb value.
* @param maximumThumbValue The maximum thumb value.
* @param lowerThumbValue The lower thumb value.
* @param lowerThumbFeatheringValue The lower thumb feathering value.
* @param upperThumbValue The upper thumb value.
* @param upperThumbFeatheringValue The upper thumb feathering value.
* @param minimumTrackValue The minimum track value.
* @param maximumTrackValue The maximum track value.
* @param trackValue The track value.
* @param trackValueWraps The modulo value to mod the track value by.
* @throws IllegalArgumentException if any constraint is violated.
*/
public void setProperties( int minimumThumbValue, int maximumThumbValue,
int lowerThumbValue,
int lowerThumbFeatheringValue,
int upperThumbValue,
int upperThumbFeatheringValue,
int minimumTrackValue, int maximumTrackValue,
int trackValue, boolean trackValueWraps ) {
boolean changed = m_model.setThumbProperties(
minimumThumbValue, maximumThumbValue,
lowerThumbValue, lowerThumbFeatheringValue,
upperThumbValue, upperThumbFeatheringValue
);
changed = m_model.setTrackProperties(
minimumTrackValue, maximumTrackValue, trackValue
) || changed;
if ( changed )
fireStateChanged();
}
/**
* Sets the {@link Track} to use.
*
* @param t The new {@link Track}.
* @see #getTrack()
*/
public void setTrack( Track t ) {
m_track = t;
}
/**
* Sets the track value.
*
* @param newValue The new track value.
* @see #getTrackValue()
*/
public void setTrackValue( int newValue ) {
if ( m_model.setTrackValue( newValue ) )
fireStateChanged();
}
/**
* Sets the upper thumb feathering value. The following constraints must
* always hold:
* <blockquote>
* min <= LTFV <= LTV <= UTV <= UTFV <= max
* </blockquote>
*
* @param newValue The new upper thumb feathering value.
* @throws IllegalArgumentException if any constraint is violated.
* @see #getUpperThumbFeatheringValue()
* @see #setUpperThumbValue(int)
*/
public void setUpperThumbFeatheringValue( int newValue ) {
if ( m_model.setUpperThumbFeatheringValue( newValue ) )
fireStateChanged();
}
/**
* Sets the upper thumb value. The following constraints must always hold:
* <blockquote>
* min <= LTFV <= LTV <= UTV <= UTFV <= max
* </blockquote>
* @param newValue The new upper thumb value.
* @throws IllegalArgumentException if any constraint is violated.
* @see #getUpperThumbValue()
* @see #setUpperThumbFeatheringValue(int)
*/
public void setUpperThumbValue( int newValue ) {
if ( m_model.setUpperThumbValue( newValue ) )
fireStateChanged();
}
////////// protected //////////////////////////////////////////////////////
/**
* {@inheritDoc}
*/
protected void paintComponent( Graphics g ) {
if ( m_polygons == null ) {
positionPolygons();
if ( m_polygons == null )
return;
}
final Graphics2D g2d = (Graphics2D)g;
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON
);
if ( isOpaque() ) {
g2d.setColor( getBackground() );
g2d.fillRect( 0, 0, getWidth(), getHeight() );
}
//
// Paint the track.
//
final Insets in = getInsets();
final Rectangle trackRect = new Rectangle(
getMinimumThumbX(),
in.top,
getWidth() - in.left - in.right - ARROW_WIDTH * 2 - THUMB_WIDTH,
getHeight() - in.top - in.bottom
);
m_track.paintTrack( this, trackRect, g2d );
//
// Paint the thumbs.
//
g2d.setColor( Color.BLACK );
for ( int i = THUMB_INDEX_START; i <= THUMB_INDEX_END; ++i ) {
final Rectangle r = m_polygons[i].getBounds();
g2d.drawImage( m_images[ i % 2 ], null, r.x, r.y );
}
//
// Paint the arrows.
//
if ( m_trackValueWraps ) {
g2d.setColor( ARROW_COLOR );
g2d.fill( m_polygons[ LOWER_SCROLL_ARROW ] );
g2d.fill( m_polygons[ UPPER_SCROLL_ARROW ] );
}
//
// Highlight the polygon the mouse is hovering over, if any.
//
for ( Shape s : m_polygons )
if ( s == m_hoverPolygon ) {
g2d.setColor( HOVER_COLOR );
g2d.draw( s );
break;
}
}
/**
* Only one {@link ChangeEvent} is needed per slider instance since the
* event's only (read-only) state is the source property. The source of
* events generated here is always "this". The event is lazily created the
* first time that an event notification is fired.
*
* @see #fireStateChanged()
*/
protected transient ChangeEvent m_changeEvent;
////////// private ////////////////////////////////////////////////////////
/**
* A <code>LocalMouseListener</code> handles mouse events for the
* {@link RangeSelector}.
*/
private final class LocalMouseListener extends MouseAdapter
implements MouseMotionListener
{
////////// MouseAdapter methods ///////////////////////////////////////
/**
* Handle a mouse-pressed event: either the start of a drag or a click
* in one of the arrows.
*
* @param me The {@link MouseEvent}.
*/
public void mousePressed( MouseEvent me ) {
if ( me.isPopupTrigger() || ignoreMouseEvent() )
return;
final Point p = me.getPoint();
m_mousePointX = p.x;
m_foundPolygonIndex = findPolygonIndex( p );
switch ( m_foundPolygonIndex ) {
case LOWER_SCROLL_ARROW:
case UPPER_SCROLL_ARROW:
scrollOnce( m_foundPolygonIndex );
startScrollTimer( m_foundPolygonIndex );
break;
}
}
/**
* Handle a mouse-released event: kill the scroll timer, if any.
*
* @param me The {@link MouseEvent}.
*/
public void mouseReleased( MouseEvent me ) {
killScrollTimer();
}
////////// MouseMotionListener methods ////////////////////////////////
/**
* {@inheritDoc}
*/
public void mouseDragged( MouseEvent me ) {
if ( ignoreMouseEvent() || m_foundPolygonIndex == -1 )
return;
final int newDragPointX = me.getPoint().x;
m_inDrag = true;
try {
switch ( m_foundPolygonIndex ) {
case LOWER_SCROLL_ARROW:
case UPPER_SCROLL_ARROW:
// do nothing
break;
case TRACK:
dragAllThumbs( newDragPointX );
break;
default:
dragThumb( newDragPointX );
}
}
catch ( IllegalArgumentException e ) {
// ignore
}
m_inDrag = false;
/*
if ( m_stateChanged )
fireStateChanged();
*/
}
/**
* {@inheritDoc}
*/
public void mouseExited( MouseEvent me ) {
if ( m_hoverPolygon != null ) {
m_hoverPolygon = null;
repaint();
}
}
/**
* {@inheritDoc}
*/
public void mouseMoved( MouseEvent me ) {
if ( ignoreMouseEvent() )
return;
final Shape oldHoverPolygon = m_hoverPolygon;
m_hoverPolygon = findPolygon( me.getPoint() );
if ( m_hoverPolygon != oldHoverPolygon )
repaint();
}
////////// private ////////////////////////////////////////////////////
/**
* Drag all of the thumbs.
*
* @param newDragPointX The X coordinate of the mouse while dragging.
*/
private void dragAllThumbs( int newDragPointX ) {
final int minX = getMinimumThumbX();
final int maxX = getMaximumThumbX();
final int lfvx = getLowerThumbFeatheringValueX();
final int lvx = getLowerThumbValueX();
final int uvx = getUpperThumbValueX();
final int ufvx = getUpperThumbFeatheringValueX();
int dx = newDragPointX - m_mousePointX;
final int[] newX = new int[5];
if ( dx < 0 ) { ////////// Dragging to the left
newX[ LOWER_FEATHERING_THUMB ] = lfvx + dx;
if ( newX[ LOWER_FEATHERING_THUMB ] < minX ) {
//
// Prevent the thumb from being dragged below the minimum
// X.
//
newX[ LOWER_FEATHERING_THUMB ] = minX;
dx = minX - lfvx;
newDragPointX = m_mousePointX + dx;
}
newX[ LOWER_THUMB ] = lvx + dx;
newX[ UPPER_THUMB ] = uvx + dx;
newX[ UPPER_FEATHERING_THUMB ] = ufvx + dx;
} else { ////////// Dragging to the right
newX[ UPPER_FEATHERING_THUMB ] = ufvx + dx;
if ( newX[ UPPER_FEATHERING_THUMB ] > maxX ) {
//
// Prevent the thumb from being dragged above the maximum
// X.
//
newX[ UPPER_FEATHERING_THUMB ] = maxX;
dx = maxX - ufvx;
newDragPointX = m_mousePointX + dx;
}
newX[ LOWER_FEATHERING_THUMB ] = lfvx + dx;
newX[ LOWER_THUMB ] = lvx + dx;
newX[ UPPER_THUMB ] = uvx + dx;
}
final int[] newV = new int[4];
for ( int i = THUMB_INDEX_START; i <= THUMB_INDEX_END; ++i ) {
newV[i] = mapXToThumbValue( newX[i] );
movePolygonTo( i, newX[i] );
}
setProperties(
getMinimumThumbValue(), getMaximumThumbValue(),
newV[ LOWER_THUMB ],
newV[ LOWER_FEATHERING_THUMB ],
newV[ UPPER_THUMB ],
newV[ UPPER_FEATHERING_THUMB ],
getMinimumTrackValue(), getMaximumTrackValue(),
getTrackValue(), getTrackValueWraps()
);
m_mousePointX = newDragPointX;
}
/**
* Drag one of the thumbs.
*
* @param newDragPointX The X coordinate of the mouse while dragging.
*/
private void dragThumb( int newDragPointX ) {
final int minX = getMinimumThumbX();
final int maxX = getMaximumThumbX();
final int lfvx = getLowerThumbFeatheringValueX();
final int lvx = getLowerThumbValueX();
final int uvx = getUpperThumbValueX();
final int ufvx = getUpperThumbFeatheringValueX();
int dx = newDragPointX - m_mousePointX;
int newX = 0;
int partnerIndex = -1;
int partnerNewX = 0;
if ( dx < 0 ) { ////////// Dragging to the left
switch ( m_foundPolygonIndex ) {
case LOWER_FEATHERING_THUMB: {
newX = lfvx + dx;
if ( newX < minX ) {
//
// Prevent the thumb from being dragged below the
// minimum X.
//
newX = newDragPointX = minX;
}
final int lfv2 = mapXToThumbValue( newX );
setLowerThumbFeatheringValue( lfv2 );
break;
}
case LOWER_THUMB: {
newX = lvx + dx;
if ( newX < minX ) {
//
// Prevent the thumb from being dragged below the
// minimum X.
//
newX = newDragPointX = minX;
dx = minX - lvx;
}
final int lv2 = mapXToThumbValue( newX );
if ( lfvx > minX ) {
//
// Dragging the lower thumb drags the lower
// feathering thumb also.
//
partnerNewX = lfvx + dx;
if ( partnerNewX < minX )
partnerNewX = minX;
final int lfv2 = mapXToThumbValue( partnerNewX );
try {
setLowerThumbFeatheringValue( lfv2 );
partnerIndex = LOWER_FEATHERING_THUMB;
}
catch ( IllegalArgumentException e ) {
// ignore
}
}
if ( getLowerThumbFeatheringValue() > lv2 ) {
setLowerThumbFeatheringValue( lv2 );
partnerIndex = LOWER_FEATHERING_THUMB;
}
setLowerThumbValue( lv2 );
break;
}
case UPPER_THUMB: {
newX = uvx + dx;
if ( newX < lvx + THUMB_WIDTH ) {
//
// Prevent the upper thumb from hitting the lower
// thumb.
//
newX = newDragPointX = lvx + THUMB_WIDTH + 1;
dx = newX - uvx;
}
//
// Dragging the upper thumb drags the upper feathering
// thumb also.
//
partnerIndex = UPPER_FEATHERING_THUMB;
partnerNewX = ufvx + dx;
final int uv2 = mapXToThumbValue( newX );
final int ufv2 = mapXToThumbValue( partnerNewX );
setUpperThumbValue( uv2 );
setUpperThumbFeatheringValue( ufv2 );
break;
}
case UPPER_FEATHERING_THUMB: {
newX = ufvx + dx;
int ufv2 = mapXToThumbValue( newX );
if ( newX <= uvx ) {
//
// The upper feathering thumb bumped into the upper
// thumb: "push" the upper thumb along.
//
if ( newX < lvx + THUMB_WIDTH ) {
//
// Prevent the upper thumb from hitting the
// lower thumb.
//
newX = lvx + THUMB_WIDTH + 1;
newDragPointX = newX;
ufv2 = mapXToThumbValue( newX );
}
setUpperThumbValue( ufv2 );
partnerIndex = UPPER_THUMB;
partnerNewX = newX;
}
setUpperThumbFeatheringValue( ufv2 );
break;
}
}
} else { ////////// Dragging to the right
switch ( m_foundPolygonIndex ) {
case LOWER_FEATHERING_THUMB: {
newX = lfvx + dx;
int lfv2 = mapXToThumbValue( newX );
if ( newX >= lvx ) {
//
// The lower feathering thumb bumped into the lower
// thumb: "push" the lower thumb along.
//
if ( newX >= uvx - THUMB_WIDTH ) {
//
// Prevent the lower thumb from hitting the
// upper thumb.
//
newX = uvx - THUMB_WIDTH - 1;
newDragPointX = newX;
lfv2 = mapXToThumbValue( newX );
}
setLowerThumbValue( lfv2 );
partnerIndex = LOWER_THUMB;
partnerNewX = newX;
}
setLowerThumbFeatheringValue( lfv2 );
break;
}
case LOWER_THUMB: {
newX = lvx + dx;
if ( newX > uvx - THUMB_WIDTH ) {
//
// Prevent the lower thumb from hitting the upper
// thumb.
//
newX = newDragPointX = uvx - THUMB_WIDTH - 1;
dx = newX - lvx;
}
//
// Dragging the lower thumb drags the lower feathering
// thumb also.
//
partnerIndex = LOWER_FEATHERING_THUMB;
partnerNewX = lfvx + dx;
final int lv2 = mapXToThumbValue( newX );
final int lfv2 = mapXToThumbValue( partnerNewX );
setLowerThumbValue( lv2 );
setLowerThumbFeatheringValue( lfv2 );
break;
}
case UPPER_THUMB: {
newX = uvx + dx;
if ( newX > maxX ) {
//
// Prevent the thumb from being dragged above the
// maximum X.
//
newX = newDragPointX = maxX;
dx = maxX - uvx;
}
final int uv2 = mapXToThumbValue( newX );
if ( ufvx < maxX ) {
//
// Dragging the upper thumb drags the upper
// feathering thumb also.
//
partnerNewX = ufvx + dx;
if ( partnerNewX > maxX )
partnerNewX = maxX;
final int ufv2 = mapXToThumbValue( partnerNewX );
try {
setUpperThumbFeatheringValue( ufv2 );
partnerIndex = UPPER_FEATHERING_THUMB;
}
catch ( IllegalArgumentException e ) {
// ignore
}
}
if ( getUpperThumbFeatheringValue() < uv2 ) {
setUpperThumbFeatheringValue( uv2 );
partnerIndex = UPPER_FEATHERING_THUMB;
}
setUpperThumbValue( uv2 );
break;
}
case UPPER_FEATHERING_THUMB: {
newX = ufvx + dx;
if ( newX > maxX ) {
//
// Prevent the thumb from being dragged above the
// maximum X.
//
newX = newDragPointX = maxX;
}
final int ufv2 = mapXToThumbValue( newX );
setUpperThumbFeatheringValue( ufv2 );
break;
}
}
}
movePolygonTo( m_foundPolygonIndex, newX );
if ( partnerIndex != -1 )
movePolygonTo( partnerIndex, partnerNewX );
m_mousePointX = newDragPointX;
}
/**
* Checks whether we should ignore all mouse events.
*
* @return Returns <code>true</code> only if all mouse events should be
* ignored.
*/
private boolean ignoreMouseEvent() {
return !isEnabled() || m_polygons == null;
}
/**
* Kill the scroll timer.
*
* @see #startScrollTimer(int)
*/
private void killScrollTimer() {
if ( m_scrollTimer != null ) {
m_scrollTimer.stop();
m_scrollTimer = null;
}
}
/**
* Scroll the track once.
*
* @param polygonIndex The index of the polygon of the arrow the user
* clicked on.
*/
private void scrollOnce( int polygonIndex ) {
final int tMin = getMinimumTrackValue();
final int tMax = getMaximumTrackValue();
int tv = getTrackValue();
switch ( polygonIndex ) {
case LOWER_SCROLL_ARROW:
tv += SCROLL_TRACK_DELTA;
if ( tv > tMax )
tv -= tMax;
break;
case UPPER_SCROLL_ARROW:
tv -= SCROLL_TRACK_DELTA;
if ( tv < tMin )
tv += tMax;
break;
}
setTrackValue( tv );
}
/**
* Start a timer first to wait to see if the user is clicking and
* holding the mouse down on one of the arrows (during which time,
* nothing happens) and then to start and continue scrolling until the
* user releases the mouse.
*
* @param polygonIndex The index of the polygon of the arrow the user
* clicked on.
* @see #killScrollTimer()
*/
private void startScrollTimer( final int polygonIndex ) {
final ActionListener listener = new ActionListener() {
boolean m_firstTime = true;
public void actionPerformed( ActionEvent ae ) {
if ( m_firstTime )
m_firstTime = false;
else
scrollOnce( polygonIndex );
}
};
m_scrollTimer = new Timer( INTER_SCROLL_DELAY, listener );
m_scrollTimer.setCoalesce( true );
m_scrollTimer.setInitialDelay( CLICK_DELAY );
m_scrollTimer.start();
}
/**
* The recent X coordinate of the mouse.
*/
private int m_mousePointX;
/**
* The index into the <code>m_polygons</code> array of the polygon
* that the mouse is in or -1 if none.
*/
private int m_foundPolygonIndex = -1;
/**
* A {@link Timer} used for scrolling the track.
*/
private Timer m_scrollTimer;
/**
* The number of milliseconds to wait starting from when the user first
* clicks to start scrolling (assuming the user keeps holding the mouse
* down).
*/
private static final int CLICK_DELAY = 500;
/**
* The number of milliseconds to wait between scrolling the track by
* {@link #SCROLL_TRACK_DELTA}.
*/
private static final int INTER_SCROLL_DELAY = 50;
/**
* The amount by which to change the track value per scroll.
*/
private static final int SCROLL_TRACK_DELTA = 5;
}
/**
* Creates the polygons.
*/
private static Polygon[] createPolygons() {
final Polygon[] p = new Polygon[7];
//
// The polygons for the lower feathering value and the upper value use
// negative coordinates so that the side of the polygon where a zero
// value would be (the right side) is at X coordinate zero.
//
p[ LOWER_FEATHERING_THUMB ] = new Polygon(
new int[]{ 0, 0, -THUMB_WIDTH / 2 },
new int[]{ 0, THUMB_HEIGHT, 0 },
3
);
p[ LOWER_THUMB ] = new Polygon(
new int[]{ 0, 0, THUMB_WIDTH / 2 },
new int[]{ 0, THUMB_HEIGHT, 0 },
3
);
p[ UPPER_THUMB ] = new Polygon(
new int[]{ 0, 0, -THUMB_WIDTH / 2 },
new int[]{ 0, THUMB_HEIGHT, 0 },
3
);
p[ UPPER_FEATHERING_THUMB ] = new Polygon(
new int[]{ 0, 0, THUMB_WIDTH / 2 },
new int[]{ 0, THUMB_HEIGHT, 0 },
3
);
p[ LOWER_SCROLL_ARROW ] = new Polygon(
new int[]{ 0, ARROW_WIDTH, ARROW_WIDTH },
new int[]{ ARROW_HEIGHT / 2, 0, ARROW_HEIGHT },
3
);
p[ UPPER_SCROLL_ARROW ] = new Polygon(
new int[]{ 0, -ARROW_WIDTH, -ARROW_WIDTH },
new int[]{ ARROW_HEIGHT / 2, 0, ARROW_HEIGHT },
3
);
return p;
}
/**
* Find the polygon the given point is within.
*
* @param p The {@link Point} to check.
* @return Returns said {@link Shape} or <code>null</code> if the point is
* not within any polygon.
* @see #findPolygonIndex(Point)
*/
private Shape findPolygon( Point p ) {
final int i = findPolygonIndex( p );
return i >= 0 ? m_polygons[i] : null;
}
/**
* Find the index of the polygon the given point is within.
*
* @param p The {@link Point} to check.
* @return Returns an integer > 0 if the point is within a polygon or -1
* if not.
* @see #findPolygon(Point)
*/
private int findPolygonIndex( Point p ) {
for ( int i = 0; i < m_polygons.length; ++i )
if ( m_polygons[i].getBounds().contains( p ) ) {
switch ( i ) {
case LOWER_SCROLL_ARROW:
case UPPER_SCROLL_ARROW:
if ( !m_trackValueWraps )
break;
// no break;
default:
return i;
}
}
return -1;
}
/**
* Sends a {@link ChangeEvent}, whose source is this component, to each
* listener. This method method is called each time a {@link ChangeEvent}
* is received from the model.
*
* @see #addChangeListener(ChangeListener)
*/
private void fireStateChanged() {
/*
if ( m_inDrag ) {
m_stateChanged = true;
return;
}
*/
final Object[] listeners = listenerList.getListenerList();
for ( int i = listeners.length - 2; i >= 0; i -= 2 ) {
if ( listeners[i] == ChangeListener.class ) {
if ( m_changeEvent == null )
m_changeEvent = new ChangeEvent( this );
final ChangeListener listener = (ChangeListener)listeners[i+1];
listener.stateChanged( m_changeEvent );
}
}
stateChanged();
}
/**
* Gets the maximum X coordinate for a thumb.
*
* @return Returns said coordinate.
* @see #getMinimumThumbX()
*/
private int getMaximumThumbX() {
final Insets in = getInsets();
return getWidth() - in.right - 1 - ARROW_WIDTH - THUMB_WIDTH / 2;
}
/**
* Gets the minimum X coordinate for a thumb.
*
* @return Returns said coordinate.
* @see #getMaximumThumbX()
*/
private int getMinimumThumbX() {
final Insets in = getInsets();
return in.left + ARROW_WIDTH + THUMB_WIDTH / 2;
}
/**
* Gets the X scaling factor for the thumbs.
*
* @return Returns said factor.
*/
private double getThumbXScale() {
final double modelWidth =
m_model.getMaximumThumbValue() - m_model.getMinimumThumbValue();
final Insets in = getInsets();
return (getWidth() - in.left - in.right
- ARROW_WIDTH * 2 - THUMB_WIDTH) / modelWidth;
}
/**
* Initialize the images for the thumbs.
*/
private static void initImages() {
final Polygon triangle = new Polygon(
new int[]{ 0, THUMB_WIDTH, THUMB_WIDTH / 2 },
new int[]{ 0, 0, THUMB_HEIGHT },
3
);
final BufferedImage buf = new BufferedImage(
THUMB_WIDTH, THUMB_HEIGHT + SHADOW_FUDGE_Y,
BufferedImage.TYPE_INT_ARGB
);
Graphics2D g2d = (Graphics2D)buf.getGraphics();
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
);
final GradientPaint p = new GradientPaint(
0, 0, THUMB_LIGHT_COLOR, THUMB_WIDTH / 2, 0, THUMB_DARK_COLOR
);
g2d.setPaint( p );
g2d.fill( triangle );
g2d.dispose();
final Raster[] raster = new Raster[2];
raster[0] = buf.getData(
new Rectangle( 0, 0, THUMB_WIDTH / 2, THUMB_HEIGHT )
);
raster[1] = buf.getData(
new Rectangle( THUMB_WIDTH / 2, 0, THUMB_WIDTH / 2, THUMB_HEIGHT )
);
raster[1] = raster[1].createTranslatedChild( 0, 0 );
final ShadowFactory sf =
new ShadowFactory( 4, 0.6F, THUMB_SHADOW_COLOR );
sf.setRenderingHint(
ShadowFactory.KEY_BLUR_QUALITY,
ShadowFactory.VALUE_BLUR_QUALITY_HIGH
);
for ( int i = 0; i < 2; ++i ) {
m_images[i] = new BufferedImage(
buf.getColorModel(), (WritableRaster)raster[i], false, null
);
BufferedImage temp = sf.createShadow( m_images[i] );
temp = temp.getSubimage(
SHADOW_FUDGE_X,
SHADOW_FUDGE_Y,
temp.getWidth() - SHADOW_FUDGE_X,
temp.getHeight() - SHADOW_FUDGE_Y
);
g2d = (Graphics2D)temp.getGraphics();
g2d.drawRenderedImage( m_images[i], new AffineTransform() );
m_images[i] = temp;
g2d.dispose();
}
}
/**
* Map a thumb value to an X coordinate.
*
* @param value The value to map.
* @return Returns the X coordinate derived from the slider value.
* @see #mapXToThumbValue(int)
*/
private int mapThumbValueToX( int value ) {
final int minV = getMinimumThumbValue();
return getMinimumThumbX() +
(int)((value - minV) * getThumbXScale() + 0.5);
}
/**
* Map an X coordinate to a thumb value.
*
* @param x The X coordinate to map.
* @return Returns the value derived from the X coordinate.
* @see #mapThumbValueToX(int)
*/
private int mapXToThumbValue( int x ) {
final int minV = getMinimumThumbValue();
final int maxV = getMaximumThumbValue();
final int minX = getMinimumThumbX();
final int maxX = getMaximumThumbX();
if ( x < minX )
return minV - 1;
if ( x > maxX )
return maxV + 1;
return minV + (int)((x - minX) / getThumbXScale() + 0.5);
}
/**
* Move a {@link Polygon} at a given index to an X coordinate.
*
* @param polygonIndex The index of the polygon to move.
* @param x The X coordinate to move the polygon to.
* @see #movePolygonTo(Polygon,int)
*/
private void movePolygonTo( int polygonIndex, int x ) {
movePolygonTo( m_polygons[ polygonIndex ], x );
}
/**
* Move a {@link Polygon} to an X coordinate. The polygon's first vertex
* is the one moved to the specified coordinate; the remaining vertices are
* moved the same relative amount.
*
* @param p The {@link Polygon} to move.
* @param x The X coordinate to move the polygon to.
*/
private static void movePolygonTo( Polygon p, int x ) {
p.translate( x - p.xpoints[0], 0 );
}
/**
* Creates and positions the polygons based on the values in the model.
*/
private void positionPolygons() {
m_polygons = null;
if ( getWidth() == 0 )
return;
final int ltfv = m_model.getLowerThumbFeatheringValue();
final int ltv = m_model.getLowerThumbValue();
final int utv = m_model.getUpperThumbValue();
final int utfv = m_model.getUpperThumbFeatheringValue();
final int ltfvx = mapThumbValueToX( ltfv );
final int ltvx = mapThumbValueToX( ltv );
final int utvx = mapThumbValueToX( utv );
final int utfvx = mapThumbValueToX( utfv );
final int y = getHeight() / 2 - THUMB_HEIGHT / 2;
m_polygons = createPolygons();
m_polygons[ LOWER_FEATHERING_THUMB ].translate( ltfvx, y );
m_polygons[ LOWER_THUMB ].translate( ltvx, y );
m_polygons[ UPPER_THUMB ].translate( utvx, y );
m_polygons[ UPPER_FEATHERING_THUMB ].translate( utfvx, y );
final Insets in = getInsets();
final int minX = getMinimumThumbX();
final int maxX = getMaximumThumbX();
final int minY = in.top;
final int maxY = minY + THUMB_HEIGHT - 1;
m_polygons[ TRACK ] = new Polygon(
new int[]{ minX, maxX, maxX, minX },
new int[]{ minY, minY, maxY, maxY },
4
);
m_polygons[ LOWER_SCROLL_ARROW ].translate( in.left, y );
m_polygons[ UPPER_SCROLL_ARROW ].translate( getWidth() - in.right, y );
}
/**
* The state changed: reinitialize stuff.
*/
private void stateChanged() {
positionPolygons();
repaint();
}
/**
* One of the {@link #m_polygons} the mouse is hovering over or
* <code>null</code> if none.
*/
private Shape m_hoverPolygon;
/**
* TODO
*/
private boolean m_inDrag;
private boolean m_stateChanged;
/**
* The {@link RangeSelectorModel} in use.
*/
private RangeSelectorModel m_model;
/**
* These polygons are used for the various parts of the component that the
* user can manipulate (click, drag, etc.).
*/
private Polygon[] m_polygons;
/**
* The {@link Track} in use or <code>null</code> if none.
*/
private Track m_track;
/**
* If <code>true</code>, then the track value wraps around when dragged
* past the ends.
*/
private boolean m_trackValueWraps;
private static final BufferedImage[] m_images = new BufferedImage[2];
private static final Color ARROW_COLOR = Color.LIGHT_GRAY;
/**
* The height of an arrow.
*/
private static final int ARROW_HEIGHT = 9;
/**
* The width of an arrow.
*/
private static final int ARROW_WIDTH = 5;
/**
* The color used to draw the highlight of the polygon the mouse is
* hovering over.
*/
private static final Color HOVER_COLOR = new Color(
Color.LIGHT_GRAY.getRed(),
Color.LIGHT_GRAY.getGreen(),
Color.LIGHT_GRAY.getBlue(),
128
);
private static final int SHADOW_FUDGE_X = 3;
private static final int SHADOW_FUDGE_Y = 2;
private static final Color THUMB_DARK_COLOR = new Color( 110, 110, 110 );
private static final Color THUMB_LIGHT_COLOR = new Color( 185, 185, 185 );
private static final Color THUMB_SHADOW_COLOR = new Color( 25, 25, 25 );
/**
* The height of a thumb.
*/
private static final int THUMB_HEIGHT = 9;
/**
* This is the combined width of both halves of a thumb, e.g., the lower
* feathering thumb and the lower thumb together; therefore, it should
* always be an even number.
*/
private static final int THUMB_WIDTH = 14;
private static final int THUMB_INDEX_START = 0;
private static final int THUMB_INDEX_END = 3;
//
// Indicies into the m_polygons array.
//
private static final int LOWER_FEATHERING_THUMB = 0;
private static final int LOWER_THUMB = 1;
private static final int UPPER_THUMB = 2;
private static final int UPPER_FEATHERING_THUMB = 3;
private static final int TRACK = 4;
private static final int LOWER_SCROLL_ARROW = 5;
private static final int UPPER_SCROLL_ARROW = 6;
static {
initImages();
}
////////// main() for testing /////////////////////////////////////////////
public static void main( String[] args ) throws Exception {
UIManager.setLookAndFeel( Platform.getPlatform().getLookAndFeel() );
/*
final RepaintManager rm = RepaintManager.currentManager( null );
rm.setDoubleBufferingEnabled( false );
*/
final RangeSelector rs =
new RangeSelector( 0, 100, 0, 0, 100, 100, 0, 100, 0, true );
rs.setTrack( new RangeSelectorColorGradientTrack() );
//rs.setTrack( new RangeSelectorZoneTrack() );
final JPanel customLayout = new JPanel( null ) {
public Dimension getPreferredSize() {
return rs.getPreferredSize();
}
public void doLayout() {
final Dimension size = getSize();
final int midY = size.height / 2;
final Dimension pSize = rs.getPreferredSize();
final int pHeight = pSize.height;
final int y = midY - pHeight / 2;
rs.setLocation( 0, y );
rs.setSize( size.width, pHeight );
}
};
customLayout.add( rs );
customLayout.setBackground( LightZoneSkin.Colors.ToolPanesBackground );
final JFrame frame = new JFrame( "Test" );
frame.getContentPane().add( customLayout );
frame.setSize( 600, 100 );
frame.setLocationRelativeTo( null );
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
frame.setVisible( true );
}
}
/* vim:set et sw=4 ts=4: */