/* * Copyright (c) 2000, 2010, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javax.swing.text; import java.lang.reflect.*; import java.text.*; import java.util.*; /** * 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}. * * @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) { StringBuilder sb = new StringBuilder(); 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)) { Number numberValue = (Number)value; if (valueClass == Integer.class) { return Integer.valueOf(numberValue.intValue()); } else if (valueClass == Long.class) { return Long.valueOf(numberValue.longValue()); } else if (valueClass == Float.class) { return Float.valueOf(numberValue.floatValue()); } else if (valueClass == Double.class) { return Double.valueOf(numberValue.doubleValue()); } else if (valueClass == Byte.class) { return Byte.valueOf(numberValue.byteValue()); } else if (valueClass == Short.class) { return Short.valueOf(numberValue.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; } /** * 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--; } return size == 0; } 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 return getBufferedChar(index) == getDecimalSeparator(); } 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) { for (Object key : attrs.keySet()) { 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; } /** * 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); } }