/* * @(#)NumberFormatter.java 1.10 03/12/19 * * Copyright 2004 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.*; /** * NumberFormatter subclasses InternationalFormatter * adding special behavior for numbers. Among the specializations are * (these are only used if the NumberFormatter does not display * invalid nubers, eg setAllowsInvalid(false)): * *

* If you are displaying scientific numbers, you may wish to turn on * overwrite mode, setOverwriteMode(true). For example: *

 * DecimalFormat decimalFormat = new DecimalFormat("0.000E0");
 * NumberFormatter textFormatter = new NumberFormatter(decimalFormat);
 * textFormatter.setOverwriteMode(true);
 * textFormatter.setAllowsInvalid(false);
 * 
*

* If you are going to allow the user to enter decimal * values, you should either force the DecimalFormat to contain at least * one decimal (#.0###), or allow the value to be invalid * setAllowsInvalid(true). Otherwise users may not be able to * input decimal values. *

* NumberFormatter provides slightly different behavior to * stringToValue than that of its superclass. If you have * specified a Class for values, {@link #setValueClass}, that is one of * of Integer, Long, Float, * Double, Byte or Short and * the Format's parseObject returns an instance of * Number, the corresponding instance of the value class * will be created using the constructor appropriate for the primitive * type the value class represents. For example: * setValueClass(Integer.class) will cause the resulting * value to be created via * new Integer(((Number)formatter.parseObject(string)).intValue()). * This is typically useful if you * wish to set a min/max value as the various Number * implementations are generally not comparable to each other. This is also * useful if for some reason you need a specific Number * implementation for your values. *

* Warning: * 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 JavaBeansTM * has been added to the java.beans 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 NumberFormatter with the a default * NumberFormat instance obtained from * NumberFormat.getNumberInstance(). */ 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. *

* If you have used the nullary constructor the value of this property * will be determined for the current locale by way of the * NumberFormat.getNumberInstance() 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 parseObject on f, 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 valueClass is one of Integer, * Long, Float, Double, * Byte or Short and value * is an instanceof Number. */ 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 text contains in an invalid * character to insert, that is, it is not a digit * (Character.isDigit()) 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 NumberFormat.Field starting * index incrementing by direction. */ 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 * aChar 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); } }