jdk/src/share/classes/javax/swing/text/NumberFormatter.java
changeset 2 90ce3da70b43
child 438 2ae294e4518c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/share/classes/javax/swing/text/NumberFormatter.java	Sat Dec 01 00:00:00 2007 +0000
@@ -0,0 +1,505 @@
+/*
+ * Copyright 2000-2003 Sun Microsystems, Inc.  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.  Sun designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.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 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}.
+ *
+ * @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);
+    }
+}