| /* | |
| * Copyright (c) 2000, 2024, 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.Constructor; | |
| import java.text.AttributedCharacterIterator; | |
| import java.text.DecimalFormat; | |
| import java.text.DecimalFormatSymbols; | |
| import java.text.Format; | |
| import java.text.NumberFormat; | |
| import java.text.ParseException; | |
| import java.util.Map; | |
| 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 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 weird 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; | |
| } | |
| /** | |
| * Overridden 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 { | |
| 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, | |
| String.valueOf(aChar)); | |
| } | |
| return stringToValue(string); | |
| } | |
| } |