//package com.ryanm.util.swing;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.LinkedList;
import java.util.List;
import javax.swing.BoundedRangeModel;
import javax.swing.BoxLayout;
import javax.swing.DefaultBoundedRangeModel;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.Timer;
import javax.swing.border.TitledBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
/**
* The class makes it easy to select a numerical value, possibly from a given
* range
*
* @author ryanm
*/
public class FloatChooser extends JPanel implements ChangeListener
{
/**
* The size of steps in the spinner to aim for. What you get will depend on
* the range.
*/
private float stepSize = 0.1f;
/**
* The number of steps in the slider to aim for. What you get will depend on
* the range.
*/
private static final int SCALE = 10000;
private static final float MAX_VELOCITY = 5;
private List listeners = new LinkedList();
private JSlider slider = new JSlider();
private SpinnerNumberModel snm;
private JSpinner spinner = new JSpinner();
private boolean absolute = false;
private Timer timer = new Timer( 50, new ActionListener() {
@Override
public void actionPerformed( ActionEvent e )
{
if( e.getSource() == timer )
{
// set the value
if( velocity != 0 )
{
setValue( getValue() + velocity );
}
}
}
} );
private float velocity = 0;
private float oldValue;
private boolean integer = false;
private RangeEditor rangeEditor = new RangeEditor();
private float[] range = new float[] { Float.NaN, Float.NaN };
/**
* Controls the unbounded slider operation
*/
private MouseListener sliderListener = new MouseAdapter() {
@Override
public void mousePressed( MouseEvent e )
{
// start the Timertask
timer.start();
}
@Override
public void mouseReleased( MouseEvent e )
{
// end the timertask
timer.stop();
slider.getModel().setValue( SCALE / 2 );
}
};
private MouseListener rangeAdjustListener = new MouseAdapter() {
@Override
public void mouseReleased( MouseEvent e )
{
if( e.isPopupTrigger() && e.isShiftDown() )
{
showRangeAdjuster();
}
}
@Override
public void mouseClicked( MouseEvent e )
{
if( e.isPopupTrigger() && e.isShiftDown() )
{
showRangeAdjuster();
}
}
@Override
public void mousePressed( MouseEvent e )
{
if( e.isPopupTrigger() && e.isShiftDown() )
{
showRangeAdjuster();
}
}
private void showRangeAdjuster()
{
rangeEditor.lower.setText( snm.getMinimum() == null ? "None" : snm
.getMinimum().toString() );
rangeEditor.upper.setText( snm.getMaximum() == null ? "None" : snm
.getMaximum().toString() );
rangeEditor.setLocationRelativeTo( FloatChooser.this );
rangeEditor.setVisible( true );
}
};
/**
* Constructs a new FloatChooser
*
* @param min
* The minimum value, or null for no minimum
* @param max
* The maximum value, or null for no maximum
* @param value
* The current value
*/
public FloatChooser( Float min, Float max, float value )
{
this( min, max, value, false );
}
/**
* Constructs a new FloatChooser
*
* @param min
* The minimum vlaue, or null for no minimum
* @param max
* The maximum value, or null for no maximum
* @param value
* The current value
* @param integer
* If true
, values will be rounded to the nearest
* integer, and the velocity slider will be scaled linearly
*/
public FloatChooser( Float min, Float max, float value, boolean integer )
{
setLayout( new BoxLayout( this, BoxLayout.Y_AXIS ) );
oldValue = value;
add( spinner );
spinner.addChangeListener( this );
add( slider );
slider.addChangeListener( this );
this.integer = integer;
setRange( min, max );
JSpinner.NumberEditor ne = new JSpinner.NumberEditor( spinner, "0.#####" );
spinner.setEditor( ne );
slider.setPreferredSize( new Dimension( 120,
slider.getPreferredSize().height ) );
}
/**
* Gets the listener that will trigger the range adjust dialog. Apply it to
* what you want sensitised
*
* @return The range adjust dialog trigger
*/
public MouseListener getRangeAdjustListener()
{
return rangeAdjustListener;
}
/**
* Sets the range of possible values
*
* @param min
* The minimum possible value, or {@link Float}.NaN for no minimum
* limit
* @param max
* The maximum possible value, or {@link Float}.NaN for no maximum
* limit
*/
public void setRange( float min, float max )
{
setRange( Float.isNaN( min ) ? null : new Float( min ),
Float.isNaN( max ) ? null : new Float( max ) );
}
/**
* Sets the range of possible values
*
* @param min
* The minimum possible value, or null for no minimum limit
* @param max
* The maximum possible value, or null for no maximum limit
*/
public void setRange( Float min, Float max )
{
range[ 0 ] = min != null ? min.floatValue() : Float.NaN;
range[ 1 ] = max != null ? max.floatValue() : Float.NaN;
slider.removeChangeListener( this );
// get the limits
snm = new SpinnerNumberModel();
snm.setMinimum( min );
snm.setMaximum( max );
snm.setValue( new Float( oldValue ) );
spinner.setModel( snm );
BoundedRangeModel brm = new DefaultBoundedRangeModel();
if( min != null && max != null )
{ // put the slider into absolute mode
absolute = true;
snm.setStepSize( new Float( stepSize ) );
// build the slider
brm.setMinimum( ( int ) ( min.floatValue() * SCALE ) );
brm.setMaximum( ( int ) ( max.floatValue() * SCALE ) );
brm.setValue( ( int ) ( oldValue * SCALE ) );
slider.removeMouseListener( sliderListener );
}
else
{ // put the slider into velocity mode
absolute = false;
brm.setMinimum( 0 );
brm.setMaximum( SCALE );
brm.setValue( SCALE / 2 );
slider.addMouseListener( sliderListener );
}
slider.setPaintTrack( absolute );
slider.setModel( brm );
slider.addChangeListener( this );
validate();
synchronized( listeners )
{
for( Listener l : listeners )
{
l.rangeChanged( min == null ? Float.NaN : min.floatValue(),
max == null ? Float.NaN : max.floatValue() );
}
}
}
/**
* Gets the currently set range
*
* @return an {min,max} array, where {@link Float#NaN} signifies no limit
*/
public float[] getRange()
{
return range;
}
/**
* Gets the minimum value possible in this FloatChooser
*
* @return the minimum value possible, or null if there is no lower limit
*/
public Float getMinValue()
{
SpinnerNumberModel snm = ( SpinnerNumberModel ) spinner.getModel();
return ( Float ) snm.getMinimum();
}
/**
* Gets the maximum value possible in this FloatChooser
*
* @return the maximum value possible, or null if there is no upper limit
*/
public Float getMaxValue()
{
SpinnerNumberModel snm = ( SpinnerNumberModel ) spinner.getModel();
return ( Float ) snm.getMaximum();
}
/**
* Gets the current value
*
* @return The current value
*/
public float getValue()
{
return ( ( Number ) spinner.getValue() ).floatValue();
}
/**
* Sets the current value. If the supplied value is outside of the current
* range, the closest legal value will be set. Listeners will be notified
*
* @param value
* The value to set.
*/
public void setValue( float value )
{
if( integer )
{
value = Math.round( value );
}
if( getMinValue() != null )
{
value = Math.max( getMinValue().floatValue(), value );
}
if( getMaxValue() != null )
{
value = Math.min( getMaxValue().floatValue(), value );
}
if( value != oldValue )
{
oldValue = value;
spinner.setValue( new Float( value ) );
if( absolute )
{
slider.setValue( ( int ) ( SCALE * value ) );
}
synchronized( listeners )
{
for( Listener listener : listeners )
{
listener.valueChanged( value );
}
}
}
}
/**
* Sets the step size for the spinner's up and down buttons
*
* @param stepSize
* The new step size
*/
public void setSpinnerStepSize( float stepSize )
{
this.stepSize = stepSize;
( ( SpinnerNumberModel ) spinner.getModel() ).setStepSize( new Float(
stepSize ) );
}
/**
* Gets the step size for the spinner's up and down buttons
*
* @return The current step size
*/
public float getSpinnerStepSize()
{
return stepSize;
}
@Override
public void setEnabled( boolean enabled )
{
spinner.setEnabled( enabled );
if( slider != null )
{
slider.setEnabled( enabled );
}
}
@Override
public boolean isEnabled()
{
return spinner.isEnabled();
}
/**
* Adds a {@link Listener} to this FloatChooser. The {@link Listener} will be
* appraised of any changes to the selected value
*
* @param listener
* The {@link Listener} to add
*/
public void addListener( Listener listener )
{
synchronized( listeners )
{
listeners.add( listener );
}
}
/**
* Removes a {@link Listener} from this FloatChooser. The {@link Listener}
* will no longer be appraised of changes to the selected value
*
* @param listener
* The {@link Listener} to remove
*/
public void removeListener( Listener listener )
{
synchronized( listeners )
{
listeners.remove( listener );
}
}
@Override
public void stateChanged( ChangeEvent e )
{
Float value = null;
if( e.getSource() == spinner )
{
value = ( Float ) spinner.getValue();
}
else if( e.getSource() == slider )
{
if( absolute )
{
value = new Float( ( float ) slider.getValue() / SCALE );
}
else
{
// change the velocity
float fraction = ( ( float ) slider.getValue() - SCALE / 2 )
/ ( SCALE / 2 );
if( !integer )
{
fraction = ( float ) Math.pow( fraction, 3 );
}
velocity = fraction * MAX_VELOCITY;
if( velocity == 0 )
{
timer.stop();
}
else
{
timer.start();
}
}
}
if( value != null )
{
setValue( value.floatValue() );
}
}
/**
* Interface for keeping track of the state of this widget
*
* @author ryanm
*/
public interface Listener
{
/**
* Called when the value of the widget is changed, either by the user or
* via code
*
* @param value
* the new value
*/
public void valueChanged( float value );
/**
* Called when the valid range is changed, either by the user or via code
*
* @param low
* the new lower bound, or {@link Float#NaN} for no bound
* @param high
* the new upper bound, or {@link Float#NaN} for no bound
*/
public void rangeChanged( float low, float high );
}
private class RangeEditor extends JDialog
{
private JTextField lower = new JTextField( 16 );
private JTextField upper = new JTextField( 16 );
private JButton yes = new JButton( "OK" );
private JButton no = new JButton( "Cancel" );
private RangeEditor()
{
setTitle( "Valid Range" );
setModal( true );
setResizable( false );
lower.setBorder( new TitledBorder( "Lower bound" ) );
lower.setHorizontalAlignment( SwingConstants.CENTER );
upper.setBorder( new TitledBorder( "Upper bound" ) );
upper.setHorizontalAlignment( SwingConstants.CENTER );
no.setMnemonic( 'C' );
getRootPane().setDefaultButton( yes );
JPanel bp = new JPanel();
bp.setLayout( new FlowLayout( FlowLayout.RIGHT ) );
bp.add( no );
bp.add( yes );
no.addActionListener( new ActionListener() {
@Override
public void actionPerformed( ActionEvent e )
{
setVisible( false );
}
} );
yes.addActionListener( new ActionListener() {
@Override
public void actionPerformed( ActionEvent e )
{
Float low = null;
try
{
low = new Float( lower.getText() );
}
catch( NumberFormatException nfe )
{
}
Float high = null;
try
{
high = new Float( upper.getText() );
}
catch( NumberFormatException nfe )
{
}
if( integer )
{
if( low != null )
{
low = new Float( low.intValue() );
}
if( high != null )
{
high = new Float( high.intValue() );
}
}
setRange( low, high );
setVisible( false );
}
} );
getContentPane().setLayout(
new BoxLayout( getContentPane(), BoxLayout.Y_AXIS ) );
add( lower );
add( upper );
add( bp );
pack();
}
}
}