1 /*
   2  * Copyright (c) 2000, 2008, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package javax.swing.text;
  26 
  27 import java.lang.reflect.*;
  28 import java.text.*;
  29 import java.util.*;
  30 
  31 /**
  32  * <code>NumberFormatter</code> subclasses <code>InternationalFormatter</code>
  33  * adding special behavior for numbers. Among the specializations are
  34  * (these are only used if the <code>NumberFormatter</code> does not display
  35  * invalid nubers, eg <code>setAllowsInvalid(false)</code>):
  36  * <ul>
  37  *   <li>Pressing +/- (- is determined from the
  38  *       <code>DecimalFormatSymbols</code> associated with the
  39  *       <code>DecimalFormat</code>) in any field but the exponent
  40  *       field will attempt to change the sign of the number to
  41  *       positive/negative.
  42  *   <li>Pressing +/- (- is determined from the
  43  *       <code>DecimalFormatSymbols</code> associated with the
  44  *       <code>DecimalFormat</code>) in the exponent field will
  45  *       attemp to change the sign of the exponent to positive/negative.
  46  * </ul>
  47  * <p>
  48  * If you are displaying scientific numbers, you may wish to turn on
  49  * overwrite mode, <code>setOverwriteMode(true)</code>. For example:
  50  * <pre>
  51  * DecimalFormat decimalFormat = new DecimalFormat("0.000E0");
  52  * NumberFormatter textFormatter = new NumberFormatter(decimalFormat);
  53  * textFormatter.setOverwriteMode(true);
  54  * textFormatter.setAllowsInvalid(false);
  55  * </pre>
  56  * <p>
  57  * If you are going to allow the user to enter decimal
  58  * values, you should either force the DecimalFormat to contain at least
  59  * one decimal (<code>#.0###</code>), or allow the value to be invalid
  60  * <code>setAllowsInvalid(true)</code>. Otherwise users may not be able to
  61  * input decimal values.
  62  * <p>
  63  * <code>NumberFormatter</code> provides slightly different behavior to
  64  * <code>stringToValue</code> than that of its superclass. If you have
  65  * specified a Class for values, {@link #setValueClass}, that is one of
  66  * of <code>Integer</code>, <code>Long</code>, <code>Float</code>,
  67  * <code>Double</code>, <code>Byte</code> or <code>Short</code> and
  68  * the Format's <code>parseObject</code> returns an instance of
  69  * <code>Number</code>, the corresponding instance of the value class
  70  * will be created using the constructor appropriate for the primitive
  71  * type the value class represents. For example:
  72  * <code>setValueClass(Integer.class)</code> will cause the resulting
  73  * value to be created via
  74  * <code>new Integer(((Number)formatter.parseObject(string)).intValue())</code>.
  75  * This is typically useful if you
  76  * wish to set a min/max value as the various <code>Number</code>
  77  * implementations are generally not comparable to each other. This is also
  78  * useful if for some reason you need a specific <code>Number</code>
  79  * implementation for your values.
  80  * <p>
  81  * <strong>Warning:</strong>
  82  * Serialized objects of this class will not be compatible with
  83  * future Swing releases. The current serialization support is
  84  * appropriate for short term storage or RMI between applications running
  85  * the same version of Swing.  As of 1.4, support for long term storage
  86  * of all JavaBeans<sup><font size="-2">TM</font></sup>
  87  * has been added to the <code>java.beans</code> package.
  88  * Please see {@link java.beans.XMLEncoder}.
  89  *
  90  * @since 1.4
  91  */
  92 public class NumberFormatter extends InternationalFormatter {
  93     /** The special characters from the Format instance. */
  94     private String specialChars;
  95 
  96     /**
  97      * Creates a <code>NumberFormatter</code> with the a default
  98      * <code>NumberFormat</code> instance obtained from
  99      * <code>NumberFormat.getNumberInstance()</code>.
 100      */
 101     public NumberFormatter() {
 102         this(NumberFormat.getNumberInstance());
 103     }
 104 
 105     /**
 106      * Creates a NumberFormatter with the specified Format instance.
 107      *
 108      * @param format Format used to dictate legal values
 109      */
 110     public NumberFormatter(NumberFormat format) {
 111         super(format);
 112         setFormat(format);
 113         setAllowsInvalid(true);
 114         setCommitsOnValidEdit(false);
 115         setOverwriteMode(false);
 116     }
 117 
 118     /**
 119      * Sets the format that dictates the legal values that can be edited
 120      * and displayed.
 121      * <p>
 122      * If you have used the nullary constructor the value of this property
 123      * will be determined for the current locale by way of the
 124      * <code>NumberFormat.getNumberInstance()</code> method.
 125      *
 126      * @param format NumberFormat instance used to dictate legal values
 127      */
 128     public void setFormat(Format format) {
 129         super.setFormat(format);
 130 
 131         DecimalFormatSymbols dfs = getDecimalFormatSymbols();
 132 
 133         if (dfs != null) {
 134             StringBuilder sb = new StringBuilder();
 135 
 136             sb.append(dfs.getCurrencySymbol());
 137             sb.append(dfs.getDecimalSeparator());
 138             sb.append(dfs.getGroupingSeparator());
 139             sb.append(dfs.getInfinity());
 140             sb.append(dfs.getInternationalCurrencySymbol());
 141             sb.append(dfs.getMinusSign());
 142             sb.append(dfs.getMonetaryDecimalSeparator());
 143             sb.append(dfs.getNaN());
 144             sb.append(dfs.getPercent());
 145             sb.append('+');
 146             specialChars = sb.toString();
 147         }
 148         else {
 149             specialChars = "";
 150         }
 151     }
 152 
 153     /**
 154      * Invokes <code>parseObject</code> on <code>f</code>, returning
 155      * its value.
 156      */
 157     Object stringToValue(String text, Format f) throws ParseException {
 158         if (f == null) {
 159             return text;
 160         }
 161         Object value = f.parseObject(text);
 162 
 163         return convertValueToValueClass(value, getValueClass());
 164     }
 165 
 166     /**
 167      * Converts the passed in value to the passed in class. This only
 168      * works if <code>valueClass</code> is one of <code>Integer</code>,
 169      * <code>Long</code>, <code>Float</code>, <code>Double</code>,
 170      * <code>Byte</code> or <code>Short</code> and <code>value</code>
 171      * is an instanceof <code>Number</code>.
 172      */
 173     private Object convertValueToValueClass(Object value, Class valueClass) {
 174         if (valueClass != null && (value instanceof Number)) {
 175             Number numberValue = (Number)value;
 176             if (valueClass == Integer.class) {
 177                 return Integer.valueOf(numberValue.intValue());
 178             }
 179             else if (valueClass == Long.class) {
 180                 return Long.valueOf(numberValue.longValue());
 181             }
 182             else if (valueClass == Float.class) {
 183                 return Float.valueOf(numberValue.floatValue());
 184             }
 185             else if (valueClass == Double.class) {
 186                 return Double.valueOf(numberValue.doubleValue());
 187             }
 188             else if (valueClass == Byte.class) {
 189                 return Byte.valueOf(numberValue.byteValue());
 190             }
 191             else if (valueClass == Short.class) {
 192                 return Short.valueOf(numberValue.shortValue());
 193             }
 194         }
 195         return value;
 196     }
 197 
 198     /**
 199      * Returns the character that is used to toggle to positive values.
 200      */
 201     private char getPositiveSign() {
 202         return '+';
 203     }
 204 
 205     /**
 206      * Returns the character that is used to toggle to negative values.
 207      */
 208     private char getMinusSign() {
 209         DecimalFormatSymbols dfs = getDecimalFormatSymbols();
 210 
 211         if (dfs != null) {
 212             return dfs.getMinusSign();
 213         }
 214         return '-';
 215     }
 216 
 217     /**
 218      * Returns the character that is used to toggle to negative values.
 219      */
 220     private char getDecimalSeparator() {
 221         DecimalFormatSymbols dfs = getDecimalFormatSymbols();
 222 
 223         if (dfs != null) {
 224             return dfs.getDecimalSeparator();
 225         }
 226         return '.';
 227     }
 228 
 229     /**
 230      * Returns the DecimalFormatSymbols from the Format instance.
 231      */
 232     private DecimalFormatSymbols getDecimalFormatSymbols() {
 233         Format f = getFormat();
 234 
 235         if (f instanceof DecimalFormat) {
 236             return ((DecimalFormat)f).getDecimalFormatSymbols();
 237         }
 238         return null;
 239     }
 240 
 241     /**
 242      * Subclassed to return false if <code>text</code> contains in an invalid
 243      * character to insert, that is, it is not a digit
 244      * (<code>Character.isDigit()</code>) and
 245      * not one of the characters defined by the DecimalFormatSymbols.
 246      */
 247     boolean isLegalInsertText(String text) {
 248         if (getAllowsInvalid()) {
 249             return true;
 250         }
 251         for (int counter = text.length() - 1; counter >= 0; counter--) {
 252             char aChar = text.charAt(counter);
 253 
 254             if (!Character.isDigit(aChar) &&
 255                            specialChars.indexOf(aChar) == -1){
 256                 return false;
 257             }
 258         }
 259         return true;
 260     }
 261 
 262     /**
 263      * Subclassed to treat the decimal separator, grouping separator,
 264      * exponent symbol, percent, permille, currency and sign as literals.
 265      */
 266     boolean isLiteral(Map attrs) {
 267         if (!super.isLiteral(attrs)) {
 268             if (attrs == null) {
 269                 return false;
 270             }
 271             int size = attrs.size();
 272 
 273             if (attrs.get(NumberFormat.Field.GROUPING_SEPARATOR) != null) {
 274                 size--;
 275                 if (attrs.get(NumberFormat.Field.INTEGER) != null) {
 276                     size--;
 277                 }
 278             }
 279             if (attrs.get(NumberFormat.Field.EXPONENT_SYMBOL) != null) {
 280                 size--;
 281             }
 282             if (attrs.get(NumberFormat.Field.PERCENT) != null) {
 283                 size--;
 284             }
 285             if (attrs.get(NumberFormat.Field.PERMILLE) != null) {
 286                 size--;
 287             }
 288             if (attrs.get(NumberFormat.Field.CURRENCY) != null) {
 289                 size--;
 290             }
 291             if (attrs.get(NumberFormat.Field.SIGN) != null) {
 292                 size--;
 293             }
 294             return size == 0;
 295         }
 296         return true;
 297     }
 298 
 299     /**
 300      * Subclassed to make the decimal separator navigatable, as well
 301      * as making the character between the integer field and the next
 302      * field navigatable.
 303      */
 304     boolean isNavigatable(int index) {
 305         if (!super.isNavigatable(index)) {
 306             // Don't skip the decimal, it causes wierd behavior
 307             return getBufferedChar(index) == getDecimalSeparator();
 308         }
 309         return true;
 310     }
 311 
 312     /**
 313      * Returns the first <code>NumberFormat.Field</code> starting
 314      * <code>index</code> incrementing by <code>direction</code>.
 315      */
 316     private NumberFormat.Field getFieldFrom(int index, int direction) {
 317         if (isValidMask()) {
 318             int max = getFormattedTextField().getDocument().getLength();
 319             AttributedCharacterIterator iterator = getIterator();
 320 
 321             if (index >= max) {
 322                 index += direction;
 323             }
 324             while (index >= 0 && index < max) {
 325                 iterator.setIndex(index);
 326 
 327                 Map attrs = iterator.getAttributes();
 328 
 329                 if (attrs != null && attrs.size() > 0) {
 330                     for (Object key : attrs.keySet()) {
 331                         if (key instanceof NumberFormat.Field) {
 332                             return (NumberFormat.Field)key;
 333                         }
 334                     }
 335                 }
 336                 index += direction;
 337             }
 338         }
 339         return null;
 340     }
 341 
 342     /**
 343      * Overriden to toggle the value if the positive/minus sign
 344      * is inserted.
 345      */
 346     void replace(DocumentFilter.FilterBypass fb, int offset, int length,
 347                 String string, AttributeSet attr) throws BadLocationException {
 348         if (!getAllowsInvalid() && length == 0 && string != null &&
 349             string.length() == 1 &&
 350             toggleSignIfNecessary(fb, offset, string.charAt(0))) {
 351             return;
 352         }
 353         super.replace(fb, offset, length, string, attr);
 354     }
 355 
 356     /**
 357      * Will change the sign of the integer or exponent field if
 358      * <code>aChar</code> is the positive or minus sign. Returns
 359      * true if a sign change was attempted.
 360      */
 361     private boolean toggleSignIfNecessary(DocumentFilter.FilterBypass fb,
 362                                               int offset, char aChar) throws
 363                               BadLocationException {
 364         if (aChar == getMinusSign() || aChar == getPositiveSign()) {
 365             NumberFormat.Field field = getFieldFrom(offset, -1);
 366             Object newValue;
 367 
 368             try {
 369                 if (field == null ||
 370                     (field != NumberFormat.Field.EXPONENT &&
 371                      field != NumberFormat.Field.EXPONENT_SYMBOL &&
 372                      field != NumberFormat.Field.EXPONENT_SIGN)) {
 373                     newValue = toggleSign((aChar == getPositiveSign()));
 374                 }
 375                 else {
 376                     // exponent
 377                     newValue = toggleExponentSign(offset, aChar);
 378                 }
 379                 if (newValue != null && isValidValue(newValue, false)) {
 380                     int lc = getLiteralCountTo(offset);
 381                     String string = valueToString(newValue);
 382 
 383                     fb.remove(0, fb.getDocument().getLength());
 384                     fb.insertString(0, string, null);
 385                     updateValue(newValue);
 386                     repositionCursor(getLiteralCountTo(offset) -
 387                                      lc + offset, 1);
 388                     return true;
 389                 }
 390             } catch (ParseException pe) {
 391                 invalidEdit();
 392             }
 393         }
 394         return false;
 395     }
 396 
 397     /**
 398      * Invoked to toggle the sign. For this to work the value class
 399      * must have a single arg constructor that takes a String.
 400      */
 401     private Object toggleSign(boolean positive) throws ParseException {
 402         Object value = stringToValue(getFormattedTextField().getText());
 403 
 404         if (value != null) {
 405             // toString isn't localized, so that using +/- should work
 406             // correctly.
 407             String string = value.toString();
 408 
 409             if (string != null && string.length() > 0) {
 410                 if (positive) {
 411                     if (string.charAt(0) == '-') {
 412                         string = string.substring(1);
 413                     }
 414                 }
 415                 else {
 416                     if (string.charAt(0) == '+') {
 417                         string = string.substring(1);
 418                     }
 419                     if (string.length() > 0 && string.charAt(0) != '-') {
 420                         string = "-" + string;
 421                     }
 422                 }
 423                 if (string != null) {
 424                     Class<?> valueClass = getValueClass();
 425 
 426                     if (valueClass == null) {
 427                         valueClass = value.getClass();
 428                     }
 429                     try {
 430                         Constructor cons = valueClass.getConstructor(
 431                                               new Class[] { String.class });
 432 
 433                         if (cons != null) {
 434                             return cons.newInstance(new Object[]{string});
 435                         }
 436                     } catch (Throwable ex) { }
 437                 }
 438             }
 439         }
 440         return null;
 441     }
 442 
 443     /**
 444      * Invoked to toggle the sign of the exponent (for scientific
 445      * numbers).
 446      */
 447     private Object toggleExponentSign(int offset, char aChar) throws
 448                              BadLocationException, ParseException {
 449         String string = getFormattedTextField().getText();
 450         int replaceLength = 0;
 451         int loc = getAttributeStart(NumberFormat.Field.EXPONENT_SIGN);
 452 
 453         if (loc >= 0) {
 454             replaceLength = 1;
 455             offset = loc;
 456         }
 457         if (aChar == getPositiveSign()) {
 458             string = getReplaceString(offset, replaceLength, null);
 459         }
 460         else {
 461             string = getReplaceString(offset, replaceLength,
 462                                       new String(new char[] { aChar }));
 463         }
 464         return stringToValue(string);
 465     }
 466 }