/*
* @(#)NumberFormatter.java 1.11 05/11/17
*
* Copyright 2006 Sun Microsystems, Inc. All rights reserved.
* SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package javax.swing.text;
import java.lang.reflect.*;
import java.text.*;
import java.util.*;
import javax.swing.text.*;
/**
* <code>NumberFormatter</code> subclasses <code>InternationalFormatter</code>
* adding special behavior for numbers. Among the specializations are
* (these are only used if the <code>NumberFormatter</code> does not display
* invalid nubers, eg <code>setAllowsInvalid(false)</code>):
* <ul>
* <li>Pressing +/- (- is determined from the
* <code>DecimalFormatSymbols</code> associated with the
* <code>DecimalFormat</code>) in any field but the exponent
* field will attempt to change the sign of the number to
* positive/negative.
* <li>Pressing +/- (- is determined from the
* <code>DecimalFormatSymbols</code> associated with the
* <code>DecimalFormat</code>) in the exponent field will
* attemp to change the sign of the exponent to positive/negative.
* </ul>
* <p>
* If you are displaying scientific numbers, you may wish to turn on
* overwrite mode, <code>setOverwriteMode(true)</code>. For example:
* <pre>
* DecimalFormat decimalFormat = new DecimalFormat("0.000E0");
* NumberFormatter textFormatter = new NumberFormatter(decimalFormat);
* textFormatter.setOverwriteMode(true);
* textFormatter.setAllowsInvalid(false);
* </pre>
* <p>
* If you are going to allow the user to enter decimal
* values, you should either force the DecimalFormat to contain at least
* one decimal (<code>#.0###</code>), or allow the value to be invalid
* <code>setAllowsInvalid(true)</code>. Otherwise users may not be able to
* input decimal values.
* <p>
* <code>NumberFormatter</code> provides slightly different behavior to
* <code>stringToValue</code> than that of its superclass. If you have
* specified a Class for values, {@link #setValueClass}, that is one of
* of <code>Integer</code>, <code>Long</code>, <code>Float</code>,
* <code>Double</code>, <code>Byte</code> or <code>Short</code> and
* the Format's <code>parseObject</code> returns an instance of
* <code>Number</code>, the corresponding instance of the value class
* will be created using the constructor appropriate for the primitive
* type the value class represents. For example:
* <code>setValueClass(Integer.class)</code> will cause the resulting
* value to be created via
* <code>new Integer(((Number)formatter.parseObject(string)).intValue())</code>.
* This is typically useful if you
* wish to set a min/max value as the various <code>Number</code>
* implementations are generally not comparable to each other. This is also
* useful if for some reason you need a specific <code>Number</code>
* implementation for your values.
* <p>
* <strong>Warning:</strong>
* Serialized objects of this class will not be compatible with
* future Swing releases. The current serialization support is
* appropriate for short term storage or RMI between applications running
* the same version of Swing. As of 1.4, support for long term storage
* of all JavaBeans<sup><font size="-2">TM</font></sup>
* has been added to the <code>java.beans</code> package.
* Please see {@link java.beans.XMLEncoder}.
*
* @version 1.4 03/05/01
* @since 1.4
*/
public class NumberFormatter extends InternationalFormatter {
/** The special characters from the Format instance. */
private String specialChars;
/**
* Creates a <code>NumberFormatter</code> with the a default
* <code>NumberFormat</code> instance obtained from
* <code>NumberFormat.getNumberInstance()</code>.
*/
public NumberFormatter() {
this(NumberFormat.getNumberInstance());
}
/**
* Creates a NumberFormatter with the specified Format instance.
*
* @param format Format used to dictate legal values
*/
public NumberFormatter(NumberFormat format) {
super(format);
setFormat(format);
setAllowsInvalid(true);
setCommitsOnValidEdit(false);
setOverwriteMode(false);
}
/**
* Sets the format that dictates the legal values that can be edited
* and displayed.
* <p>
* If you have used the nullary constructor the value of this property
* will be determined for the current locale by way of the
* <code>NumberFormat.getNumberInstance()</code> method.
*
* @param format NumberFormat instance used to dictate legal values
*/
public void setFormat(Format format) {
super.setFormat(format);
DecimalFormatSymbols dfs = getDecimalFormatSymbols();
if (dfs != null) {
StringBuffer sb = new StringBuffer();
sb.append(dfs.getCurrencySymbol());
sb.append(dfs.getDecimalSeparator());
sb.append(dfs.getGroupingSeparator());
sb.append(dfs.getInfinity());
sb.append(dfs.getInternationalCurrencySymbol());
sb.append(dfs.getMinusSign());
sb.append(dfs.getMonetaryDecimalSeparator());
sb.append(dfs.getNaN());
sb.append(dfs.getPercent());
sb.append('+');
specialChars = sb.toString();
}
else {
specialChars = "";
}
}
/**
* Invokes <code>parseObject</code> on <code>f</code>, returning
* its value.
*/
Object stringToValue(String text, Format f) throws ParseException {
if (f == null) {
return text;
}
Object value = f.parseObject(text);
return convertValueToValueClass(value, getValueClass());
}
/**
* Converts the passed in value to the passed in class. This only
* works if <code>valueClass</code> is one of <code>Integer</code>,
* <code>Long</code>, <code>Float</code>, <code>Double</code>,
* <code>Byte</code> or <code>Short</code> and <code>value</code>
* is an instanceof <code>Number</code>.
*/
private Object convertValueToValueClass(Object value, Class valueClass) {
if (valueClass != null && (value instanceof Number)) {
if (valueClass == Integer.class) {
return new Integer(((Number)value).intValue());
}
else if (valueClass == Long.class) {
return new Long(((Number)value).longValue());
}
else if (valueClass == Float.class) {
return new Float(((Number)value).floatValue());
}
else if (valueClass == Double.class) {
return new Double(((Number)value).doubleValue());
}
else if (valueClass == Byte.class) {
return new Byte(((Number)value).byteValue());
}
else if (valueClass == Short.class) {
return new Short(((Number)value).shortValue());
}
}
return value;
}
/**
* Returns the character that is used to toggle to positive values.
*/
private char getPositiveSign() {
return '+';
}
/**
* Returns the character that is used to toggle to negative values.
*/
private char getMinusSign() {
DecimalFormatSymbols dfs = getDecimalFormatSymbols();
if (dfs != null) {
return dfs.getMinusSign();
}
return '-';
}
/**
* Returns the character that is used to toggle to negative values.
*/
private char getDecimalSeparator() {
DecimalFormatSymbols dfs = getDecimalFormatSymbols();
if (dfs != null) {
return dfs.getDecimalSeparator();
}
return '.';
}
/**
* Returns the DecimalFormatSymbols from the Format instance.
*/
private DecimalFormatSymbols getDecimalFormatSymbols() {
Format f = getFormat();
if (f instanceof DecimalFormat) {
return ((DecimalFormat)f).getDecimalFormatSymbols();
}
return null;
}
/**
*/
private boolean isValidInsertionCharacter(char aChar) {
return (Character.isDigit(aChar) || specialChars.indexOf(aChar) != -1);
}
/**
* Subclassed to return false if <code>text</code> contains in an invalid
* character to insert, that is, it is not a digit
* (<code>Character.isDigit()</code>) and
* not one of the characters defined by the DecimalFormatSymbols.
*/
boolean isLegalInsertText(String text) {
if (getAllowsInvalid()) {
return true;
}
for (int counter = text.length() - 1; counter >= 0; counter--) {
char aChar = text.charAt(counter);
if (!Character.isDigit(aChar) &&
specialChars.indexOf(aChar) == -1){
return false;
}
}
return true;
}
/**
* Subclassed to treat the decimal separator, grouping separator,
* exponent symbol, percent, permille, currency and sign as literals.
*/
boolean isLiteral(Map attrs) {
if (!super.isLiteral(attrs)) {
if (attrs == null) {
return false;
}
int size = attrs.size();
if (attrs.get(NumberFormat.Field.GROUPING_SEPARATOR) != null) {
size--;
if (attrs.get(NumberFormat.Field.INTEGER) != null) {
size--;
}
}
if (attrs.get(NumberFormat.Field.EXPONENT_SYMBOL) != null) {
size--;
}
if (attrs.get(NumberFormat.Field.PERCENT) != null) {
size--;
}
if (attrs.get(NumberFormat.Field.PERMILLE) != null) {
size--;
}
if (attrs.get(NumberFormat.Field.CURRENCY) != null) {
size--;
}
if (attrs.get(NumberFormat.Field.SIGN) != null) {
size--;
}
if (size == 0) {
return true;
}
return false;
}
return true;
}
/**
* Subclassed to make the decimal separator navigatable, as well
* as making the character between the integer field and the next
* field navigatable.
*/
boolean isNavigatable(int index) {
if (!super.isNavigatable(index)) {
// Don't skip the decimal, it causes wierd behavior
if (getBufferedChar(index) == getDecimalSeparator()) {
return true;
}
return false;
}
return true;
}
/**
* Returns the first <code>NumberFormat.Field</code> starting
* <code>index</code> incrementing by <code>direction</code>.
*/
private NumberFormat.Field getFieldFrom(int index, int direction) {
if (isValidMask()) {
int max = getFormattedTextField().getDocument().getLength();
AttributedCharacterIterator iterator = getIterator();
if (index >= max) {
index += direction;
}
while (index >= 0 && index < max) {
iterator.setIndex(index);
Map attrs = iterator.getAttributes();
if (attrs != null && attrs.size() > 0) {
Iterator keys = attrs.keySet().iterator();
while (keys.hasNext()) {
Object key = keys.next();
if (key instanceof NumberFormat.Field) {
return (NumberFormat.Field)key;
}
}
}
index += direction;
}
}
return null;
}
/**
* Overriden to toggle the value if the positive/minus sign
* is inserted.
*/
void replace(DocumentFilter.FilterBypass fb, int offset, int length,
String string, AttributeSet attr) throws BadLocationException {
if (!getAllowsInvalid() && length == 0 && string != null &&
string.length() == 1 &&
toggleSignIfNecessary(fb, offset, string.charAt(0))) {
return;
}
super.replace(fb, offset, length, string, attr);
}
/**
* Will change the sign of the integer or exponent field if
* <code>aChar</code> is the positive or minus sign. Returns
* true if a sign change was attempted.
*/
private boolean toggleSignIfNecessary(DocumentFilter.FilterBypass fb,
int offset, char aChar) throws
BadLocationException {
if (aChar == getMinusSign() || aChar == getPositiveSign()) {
NumberFormat.Field field = getFieldFrom(offset, -1);
Object newValue;
try {
if (field == null ||
(field != NumberFormat.Field.EXPONENT &&
field != NumberFormat.Field.EXPONENT_SYMBOL &&
field != NumberFormat.Field.EXPONENT_SIGN)) {
newValue = toggleSign((aChar == getPositiveSign()));
}
else {
// exponent
newValue = toggleExponentSign(offset, aChar);
}
if (newValue != null && isValidValue(newValue, false)) {
int lc = getLiteralCountTo(offset);
String string = valueToString(newValue);
fb.remove(0, fb.getDocument().getLength());
fb.insertString(0, string, null);
updateValue(newValue);
repositionCursor(getLiteralCountTo(offset) -
lc + offset, 1);
return true;
}
} catch (ParseException pe) {
invalidEdit();
}
}
return false;
}
/**
* Returns true if the range offset to length identifies the only
* integer field.
*/
private boolean isOnlyIntegerField(int offset, int length) {
if (isValidMask()) {
int start = getAttributeStart(NumberFormat.Field.INTEGER);
if (start != -1) {
AttributedCharacterIterator iterator = getIterator();
iterator.setIndex(start);
if (offset > start || iterator.getRunLimit(
NumberFormat.Field.INTEGER) > (offset + length)) {
return false;
}
return true;
}
}
return false;
}
/**
* Invoked to toggle the sign. For this to work the value class
* must have a single arg constructor that takes a String.
*/
private Object toggleSign(boolean positive) throws ParseException {
Object value = stringToValue(getFormattedTextField().getText());
if (value != null) {
// toString isn't localized, so that using +/- should work
// correctly.
String string = value.toString();
if (string != null && string.length() > 0) {
if (positive) {
if (string.charAt(0) == '-') {
string = string.substring(1);
}
}
else {
if (string.charAt(0) == '+') {
string = string.substring(1);
}
if (string.length() > 0 && string.charAt(0) != '-') {
string = "-" + string;
}
}
if (string != null) {
Class valueClass = getValueClass();
if (valueClass == null) {
valueClass = value.getClass();
}
try {
Constructor cons = valueClass.getConstructor(
new Class[] { String.class });
if (cons != null) {
return cons.newInstance(new Object[]{string});
}
} catch (Throwable ex) { }
}
}
}
return null;
}
/**
* Invoked to toggle the sign of the exponent (for scientific
* numbers).
*/
private Object toggleExponentSign(int offset, char aChar) throws
BadLocationException, ParseException {
String string = getFormattedTextField().getText();
int replaceLength = 0;
int loc = getAttributeStart(NumberFormat.Field.EXPONENT_SIGN);
if (loc >= 0) {
replaceLength = 1;
offset = loc;
}
if (aChar == getPositiveSign()) {
string = getReplaceString(offset, replaceLength, null);
}
else {
string = getReplaceString(offset, replaceLength,
new String(new char[] { aChar }));
}
return stringToValue(string);
}
}