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