Swing Components Java

//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();
    }
  }
}