Java tutorial
/* * Copyright (c) 2000, 2014, 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.*; import sun.reflect.misc.ReflectUtil; import sun.swing.SwingUtilities2; /** * <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 numbers, for example, <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 * attempt 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>Integer.valueOf(((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™ * has been added to the <code>java.beans</code> package. * Please see {@link java.beans.XMLEncoder}. * * @since 1.4 */ @SuppressWarnings("serial") // Same-version serialization only 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) { 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 <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)) { 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 <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--; } return size == 0; } return true; } /** * Subclassed to make the decimal separator navigable, as well * as making the character between the integer field and the next * field navigable. */ 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 <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) { 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 * <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; } /** * 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 { ReflectUtil.checkPackageAccess(valueClass); SwingUtilities2.checkAccess(valueClass.getModifiers()); Constructor<?> cons = valueClass.getConstructor(new Class<?>[] { String.class }); if (cons != null) { SwingUtilities2.checkAccess(cons.getModifiers()); 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); } }