// (c) 2003 Allen I Holub. All rights reserved.
package com.holub.ui;
import javax.swing.*;
import javax.swing.event.*;
import java.text.*;
import java.awt.*; // for testing
import java.awt.event.*; // for testing
import javax.swing.*; // for testing
import com.holub.ui.Input;
/** This convenience class customizes {@link Input} to handle numbers.
* It provides a Customizer and also numeric constructors and
* accessors.
*
* <!-- ====================== distribution terms ===================== -->
* <p><blockquote
* style="border-style: solid; border-width:thin; padding: 1em 1em 1em 1em;">
* <center>
* Copyright © 2003, Allen I. Holub. All rights reserved.
* </center>
* <br>
* <br>
* This code is distributed under the terms of the
* <a href="http://www.gnu.org/licenses/gpl.html"
* >GNU Public License</a> (GPL)
* with the following ammendment to section 2.c:
* <p>
* As a requirement for distributing this code, your splash screen,
* about box, or equivalent must include an my name, copyright,
* <em>and URL</em>. An acceptable message would be:
* <center>
* This program contains Allen Holub's <em>XXX</em> utility.<br>
* (c) 2003 Allen I. Holub. All Rights Reserved.<br>
* http://www.holub.com<br>
* </center>
* If your progam does not run interactively, then the foregoing
* notice must appear in your documentation.
* </blockquote>
* <!-- =============================================================== -->
* @author Allen I. Holub
*/
public class NumericInput extends Input
{
/** Default precision is two decimal places */
public NumericInput(double value, final BorderStyle border, final boolean isHighlighted)
{ super( asString( value,2 ), new Behavior(), border, isHighlighted);
}
public NumericInput(long value, final BorderStyle border, final boolean isHighlighted)
{ super( asString(value), new Behavior(), border, isHighlighted);
}
/** Default precision is two decimal places */
public NumericInput(double value, Customizer c, final BorderStyle border, final boolean isHighlighted)
{ super(asString( value, 2 ), c, border, isHighlighted);
}
public NumericInput(long value, Customizer c, final BorderStyle border, final boolean isHighlighted)
{ super( asString(value), c, border, isHighlighted);
}
public NumericInput(double value, double min, double max, int precision,
final BorderStyle border, final boolean isHighlighted)
{ super(asString(value, precision), new Behavior(min,max,precision), border, isHighlighted);
}
public NumericInput(long value, double min, double max, int precision,
final BorderStyle border, final boolean isHighlighted)
{ super(asString( value ), new Behavior(min,max,precision), border, isHighlighted);
}
public static String asString( double value, int precision )
{
NumberFormat formatter = NumberFormat.getInstance();
formatter.setMinimumFractionDigits( precision );
return formatter.format( value );
}
public static String asString( long value )
{
NumberFormat formatter = NumberFormat.getInstance();
return formatter.format( value );
}
/** Convenience method, hides call to {@link NumberFormat#parse}.
* @return 0.0 if the contents of the field isn't a number.
*/
public double doubleValue()
{ try
{ return NumberFormat.getInstance().parse(getText()).doubleValue();
}
catch( ParseException e ){ return 0.0; }
}
/** Convenience method, hides call to {@link NumberFormat#parse}.
* @return 0.0 if the contents of the field isn't a number.
*/
public float floatValue()
{ try
{ return NumberFormat.getInstance().parse(getText()).floatValue();
}
catch( ParseException e ){ return 0.0F; }
}
/** Convenience method, hides call to {@link NumberFormat#parse}.
* @return 0L if the contents of the field isn't a number.
*/
public long longValue()
{ try
{ return NumberFormat.getInstance().parse(getText()).longValue();
}
catch( ParseException e ){ return 0L; }
}
/** Convenience method, hides call to {@link NumberFormat#parse}.
* @return 0 if the contents of the field isn't a number.
*/
public int intValue()
{ try
{ return NumberFormat.getInstance().parse(getText()).intValue();
}
catch( ParseException e ){ return 0; }
}
/** Replace the current value with v. Return the old value */
public long assign( long v )
{ long old = longValue();
setText(NumberFormat.getInstance().format(v));
return old;
}
/** Replace the current value with v. Return the old value */
public double assign( double v )
{ double old = doubleValue();
setText(NumberFormat.getInstance().format(v));
return old;
}
/** An implemenation of {@link Input.Customizer} for numbers.
* To be valid, the entire input string must acceptable
* to NumberFormat.parse(). The number is right justified in the
* field with the initial cursor position at the far right.
* The default tooltip is the string
* "Enter a number between <em>min</em> and <em>max</em>,"
* (or just plain "Enter a number" if you didn't specify
* any constraints).
* The help string describes what a legal number looks like.
* The number is parsed for value checking using the
* NumberFormat.parse() and the default (current) Locale.
* <p>
* The text that you extract from the Input object will be
* a legal number, but it will have commas in it. Extract
* a value using {@link NumberFormat#parse} or call one of
* the numeric-extration methods (e.g. {@link #doubleValue})
*/
static public class Behavior implements Customizer
{
private final DecimalFormat formatter = (DecimalFormat)(NumberFormat.getNumberInstance());
private double minValue = -Double.MAX_VALUE;
private double maxValue = Double.MAX_VALUE;
private int precision = -1;
private boolean isUnbounded()
{ return ( minValue == - Double.MAX_VALUE
&& maxValue == Double.MAX_VALUE );
}
/** Create a numeric customizer with a constrained range of values.
* @param minValue the minimum legal value
* @param maxValue the maximum legal value
* @param precision the maximum number of digits to
* the right of the decimal point. 0 for
* integer values, -1 for no maximum.
*/
public Behavior( double minValue, double maxValue, int precision )
{ this.minValue = minValue;
this.maxValue = maxValue;
this.precision = precision;
if (precision == 0) formatter.setParseIntegerOnly(true);
else if(precision > 0) formatter.setMaximumFractionDigits(precision);
}
/** Create a customizer with the entire
* range of values of a Double supported and no limit
* on the digits to the right of the decimal.
*/
public Behavior(){}
/** This customizer check for "valid" after every character
* is typed.
*/
public boolean validatesOnExit(){ return false; }
/** Check if the value is within limits, contains no strange
* characters, and is of the proper precision.
*/
public boolean isValid(String inputString)
{ ParsePosition position = new ParsePosition(0);
Number n = formatter.parse(inputString, position);
int index = position.getIndex();
int length = inputString.length();
char last = (char)inputString.charAt(length-1);
char point = (char)formatter.getDecimalFormatSymbols().getDecimalSeparator();
char comma = (char)formatter.getDecimalFormatSymbols().getGroupingSeparator();
char minus = (char)formatter.getDecimalFormatSymbols().getMinusSign();
if( index==length-1 && (last==comma || last==minus || last==point) ) // it's probably okay
return true;
if( index != length ) // didn't end in a separator. Garbage characters in string.
return false;
index = inputString.indexOf( point );
if( index > 0 && precision > 0 && (length-index > precision+1) )
return false; // there are characters to the right of the decimal.
double d = n.doubleValue();
return( minValue <= d && d <= maxValue );
}
public String help( String badInput )
{ StringBuffer b = new StringBuffer();
b.append( "You tried to type: " );
b.append( badInput );
if( isUnbounded() )
b.append( "<br>You must type a number.<br>" );
else
{ b.append("<br>You must type a number between ");
b.append( minAndMax() );
b.append(".<br>");
}
b.append( "Commas are okay.<br>" );
if( precision == 0)
b.append("Numbers may not contain a decimal point.<br>");
else
{ b.append("Numbers may contain a decimal point");
if( precision < 0)
b.append(".<br>");
else
{ b.append(", but<br>only " );
b.append( precision );
b.append(" digits can go the right of the decimal.");
}
}
return b.toString();
}
public void prepare( JTextField current )
{ current.setHorizontalAlignment(SwingConstants.RIGHT);
if( isUnbounded() )
current.setToolTipText("Enter a number");
else
current.setToolTipText("Enter a number between " + minAndMax() );
current.setCaretPosition(current.getText().length());
}
private String minAndMax()
{ return formatter.format( minValue )
+ " and "
+ formatter.format( maxValue );
}
}
public static class Test
{ public static void main( String[] args )
{
final NumericInput n1 = new NumericInput( 99, -100, 100, 0, UNDERLINED, true );
final NumericInput n2 = new NumericInput( 123.00F, 0, 10000, 2, BOXED, false);
final NumericInput n3 = new NumericInput( 1234.567, BORDERLESS, false);
n1.setColumns(10);
n2.setColumns(10);
n3.setColumns(10);
n1.setColumns(5);
JPanel panel = new JPanel();
panel.setLayout( new FlowLayout(FlowLayout.CENTER, 10, 10) );
panel.setBackground( Color.WHITE );
panel.add(new JLabel("integer -100<=n<=100:"));
panel.add(n1);
panel.add(new JLabel(" float (two decials) 0<=n<=10000:"));
panel.add(n2);
panel.add(new JLabel(" unbounded float:"));
panel.add(n3);
JFrame frame = new JFrame();
frame.getContentPane().setBackground( Color.WHITE );
frame.getContentPane().setLayout( new BorderLayout() );
frame.getContentPane().add(panel, BorderLayout.SOUTH );
frame.pack();
frame.show();
frame.addWindowListener
( new WindowAdapter()
{ public void windowClosing( WindowEvent e )
{ System.out.println( "n1=" + n1.intValue() );
System.out.println( "n2=" + n2.floatValue() );
System.out.println( "n3=" + n3.doubleValue() );
System.exit(0);
}
}
);
}
}
}