KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > javax > swing > text > NumberFormatter


1 /*
2  * @(#)NumberFormatter.java 1.10 03/12/19
3  *
4  * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
5  * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
6  */

7 package javax.swing.text;
8
9 import java.lang.reflect.*;
10 import java.text.*;
11 import java.util.*;
12 import javax.swing.text.*;
13
14 /**
15  * <code>NumberFormatter</code> subclasses <code>InternationalFormatter</code>
16  * adding special behavior for numbers. Among the specializations are
17  * (these are only used if the <code>NumberFormatter</code> does not display
18  * invalid nubers, eg <code>setAllowsInvalid(false)</code>):
19  * <ul>
20  * <li>Pressing +/- (- is determined from the
21  * <code>DecimalFormatSymbols</code> associated with the
22  * <code>DecimalFormat</code>) in any field but the exponent
23  * field will attempt to change the sign of the number to
24  * positive/negative.
25  * <li>Pressing +/- (- is determined from the
26  * <code>DecimalFormatSymbols</code> associated with the
27  * <code>DecimalFormat</code>) in the exponent field will
28  * attemp to change the sign of the exponent to positive/negative.
29  * </ul>
30  * <p>
31  * If you are displaying scientific numbers, you may wish to turn on
32  * overwrite mode, <code>setOverwriteMode(true)</code>. For example:
33  * <pre>
34  * DecimalFormat decimalFormat = new DecimalFormat("0.000E0");
35  * NumberFormatter textFormatter = new NumberFormatter(decimalFormat);
36  * textFormatter.setOverwriteMode(true);
37  * textFormatter.setAllowsInvalid(false);
38  * </pre>
39  * <p>
40  * If you are going to allow the user to enter decimal
41  * values, you should either force the DecimalFormat to contain at least
42  * one decimal (<code>#.0###</code>), or allow the value to be invalid
43  * <code>setAllowsInvalid(true)</code>. Otherwise users may not be able to
44  * input decimal values.
45  * <p>
46  * <code>NumberFormatter</code> provides slightly different behavior to
47  * <code>stringToValue</code> than that of its superclass. If you have
48  * specified a Class for values, {@link #setValueClass}, that is one of
49  * of <code>Integer</code>, <code>Long</code>, <code>Float</code>,
50  * <code>Double</code>, <code>Byte</code> or <code>Short</code> and
51  * the Format's <code>parseObject</code> returns an instance of
52  * <code>Number</code>, the corresponding instance of the value class
53  * will be created using the constructor appropriate for the primitive
54  * type the value class represents. For example:
55  * <code>setValueClass(Integer.class)</code> will cause the resulting
56  * value to be created via
57  * <code>new Integer(((Number)formatter.parseObject(string)).intValue())</code>.
58  * This is typically useful if you
59  * wish to set a min/max value as the various <code>Number</code>
60  * implementations are generally not comparable to each other. This is also
61  * useful if for some reason you need a specific <code>Number</code>
62  * implementation for your values.
63  * <p>
64  * <strong>Warning:</strong>
65  * Serialized objects of this class will not be compatible with
66  * future Swing releases. The current serialization support is
67  * appropriate for short term storage or RMI between applications running
68  * the same version of Swing. As of 1.4, support for long term storage
69  * of all JavaBeans<sup><font size="-2">TM</font></sup>
70  * has been added to the <code>java.beans</code> package.
71  * Please see {@link java.beans.XMLEncoder}.
72  *
73  * @version 1.4 03/05/01
74  * @since 1.4
75  */

76 public class NumberFormatter extends InternationalFormatter JavaDoc {
77     /** The special characters from the Format instance. */
78     private String JavaDoc specialChars;
79
80     /**
81      * Creates a <code>NumberFormatter</code> with the a default
82      * <code>NumberFormat</code> instance obtained from
83      * <code>NumberFormat.getNumberInstance()</code>.
84      */

85     public NumberFormatter() {
86         this(NumberFormat.getNumberInstance());
87     }
88
89     /**
90      * Creates a NumberFormatter with the specified Format instance.
91      *
92      * @param format Format used to dictate legal values
93      */

94     public NumberFormatter(NumberFormat format) {
95         super(format);
96         setFormat(format);
97         setAllowsInvalid(true);
98         setCommitsOnValidEdit(false);
99         setOverwriteMode(false);
100     }
101
102     /**
103      * Sets the format that dictates the legal values that can be edited
104      * and displayed.
105      * <p>
106      * If you have used the nullary constructor the value of this property
107      * will be determined for the current locale by way of the
108      * <code>NumberFormat.getNumberInstance()</code> method.
109      *
110      * @param format NumberFormat instance used to dictate legal values
111      */

112     public void setFormat(Format format) {
113         super.setFormat(format);
114
115         DecimalFormatSymbols dfs = getDecimalFormatSymbols();
116
117         if (dfs != null) {
118             StringBuffer JavaDoc sb = new StringBuffer JavaDoc();
119
120             sb.append(dfs.getCurrencySymbol());
121             sb.append(dfs.getDecimalSeparator());
122             sb.append(dfs.getGroupingSeparator());
123             sb.append(dfs.getInfinity());
124             sb.append(dfs.getInternationalCurrencySymbol());
125             sb.append(dfs.getMinusSign());
126             sb.append(dfs.getMonetaryDecimalSeparator());
127             sb.append(dfs.getNaN());
128             sb.append(dfs.getPercent());
129             sb.append('+');
130             specialChars = sb.toString();
131         }
132         else {
133             specialChars = "";
134         }
135     }
136
137     /**
138      * Invokes <code>parseObject</code> on <code>f</code>, returning
139      * its value.
140      */

141     Object JavaDoc stringToValue(String JavaDoc text, Format f) throws ParseException {
142         if (f == null) {
143             return text;
144         }
145         Object JavaDoc value = f.parseObject(text);
146
147         return convertValueToValueClass(value, getValueClass());
148     }
149
150     /**
151      * Converts the passed in value to the passed in class. This only
152      * works if <code>valueClass</code> is one of <code>Integer</code>,
153      * <code>Long</code>, <code>Float</code>, <code>Double</code>,
154      * <code>Byte</code> or <code>Short</code> and <code>value</code>
155      * is an instanceof <code>Number</code>.
156      */

157     private Object JavaDoc convertValueToValueClass(Object JavaDoc value, Class JavaDoc valueClass) {
158         if (valueClass != null && (value instanceof Number JavaDoc)) {
159             if (valueClass == Integer JavaDoc.class) {
160                 return new Integer JavaDoc(((Number JavaDoc)value).intValue());
161             }
162             else if (valueClass == Long JavaDoc.class) {
163                 return new Long JavaDoc(((Number JavaDoc)value).longValue());
164             }
165             else if (valueClass == Float JavaDoc.class) {
166                 return new Float JavaDoc(((Number JavaDoc)value).floatValue());
167             }
168             else if (valueClass == Double JavaDoc.class) {
169                 return new Double JavaDoc(((Number JavaDoc)value).doubleValue());
170             }
171             else if (valueClass == Byte JavaDoc.class) {
172                 return new Byte JavaDoc(((Number JavaDoc)value).byteValue());
173             }
174             else if (valueClass == Short JavaDoc.class) {
175                 return new Short JavaDoc(((Number JavaDoc)value).shortValue());
176             }
177         }
178         return value;
179     }
180
181     /**
182      * Returns the character that is used to toggle to positive values.
183      */

184     private char getPositiveSign() {
185         return '+';
186     }
187
188     /**
189      * Returns the character that is used to toggle to negative values.
190      */

191     private char getMinusSign() {
192         DecimalFormatSymbols dfs = getDecimalFormatSymbols();
193
194         if (dfs != null) {
195             return dfs.getMinusSign();
196         }
197         return '-';
198     }
199
200     /**
201      * Returns the character that is used to toggle to negative values.
202      */

203     private char getDecimalSeparator() {
204         DecimalFormatSymbols dfs = getDecimalFormatSymbols();
205
206         if (dfs != null) {
207             return dfs.getDecimalSeparator();
208         }
209         return '.';
210     }
211
212     /**
213      * Returns the DecimalFormatSymbols from the Format instance.
214      */

215     private DecimalFormatSymbols getDecimalFormatSymbols() {
216         Format f = getFormat();
217
218         if (f instanceof DecimalFormat) {
219             return ((DecimalFormat)f).getDecimalFormatSymbols();
220         }
221         return null;
222     }
223
224     /**
225      */

226     private boolean isValidInsertionCharacter(char aChar) {
227         return (Character.isDigit(aChar) || specialChars.indexOf(aChar) != -1);
228     }
229
230
231     /**
232      * Subclassed to return false if <code>text</code> contains in an invalid
233      * character to insert, that is, it is not a digit
234      * (<code>Character.isDigit()</code>) and
235      * not one of the characters defined by the DecimalFormatSymbols.
236      */

237     boolean isLegalInsertText(String JavaDoc text) {
238         if (getAllowsInvalid()) {
239             return true;
240         }
241         for (int counter = text.length() - 1; counter >= 0; counter--) {
242             char aChar = text.charAt(counter);
243
244             if (!Character.isDigit(aChar) &&
245                            specialChars.indexOf(aChar) == -1){
246                 return false;
247             }
248         }
249         return true;
250     }
251
252     /**
253      * Subclassed to treat the decimal separator, grouping separator,
254      * exponent symbol, percent, permille, currency and sign as literals.
255      */

256     boolean isLiteral(Map attrs) {
257         if (!super.isLiteral(attrs)) {
258             if (attrs == null) {
259                 return false;
260             }
261             int size = attrs.size();
262
263             if (attrs.get(NumberFormat.Field.GROUPING_SEPARATOR) != null) {
264                 size--;
265                 if (attrs.get(NumberFormat.Field.INTEGER) != null) {
266                     size--;
267                 }
268             }
269             if (attrs.get(NumberFormat.Field.EXPONENT_SYMBOL) != null) {
270                 size--;
271             }
272             if (attrs.get(NumberFormat.Field.PERCENT) != null) {
273                 size--;
274             }
275             if (attrs.get(NumberFormat.Field.PERMILLE) != null) {
276                 size--;
277             }
278             if (attrs.get(NumberFormat.Field.CURRENCY) != null) {
279                 size--;
280             }
281             if (attrs.get(NumberFormat.Field.SIGN) != null) {
282                 size--;
283             }
284             if (size == 0) {
285                 return true;
286             }
287             return false;
288         }
289         return true;
290     }
291
292     /**
293      * Subclassed to make the decimal separator navigatable, as well
294      * as making the character between the integer field and the next
295      * field navigatable.
296      */

297     boolean isNavigatable(int index) {
298         if (!super.isNavigatable(index)) {
299             // Don't skip the decimal, it causes wierd behavior
300
if (getBufferedChar(index) == getDecimalSeparator()) {
301                 return true;
302             }
303             return false;
304         }
305         return true;
306     }
307
308     /**
309      * Returns the first <code>NumberFormat.Field</code> starting
310      * <code>index</code> incrementing by <code>direction</code>.
311      */

312     private NumberFormat.Field getFieldFrom(int index, int direction) {
313         if (isValidMask()) {
314             int max = getFormattedTextField().getDocument().getLength();
315             AttributedCharacterIterator iterator = getIterator();
316
317             if (index >= max) {
318                 index += direction;
319             }
320             while (index >= 0 && index < max) {
321                 iterator.setIndex(index);
322
323                 Map attrs = iterator.getAttributes();
324
325                 if (attrs != null && attrs.size() > 0) {
326                     Iterator keys = attrs.keySet().iterator();
327
328                     while (keys.hasNext()) {
329                         Object JavaDoc key = keys.next();
330
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 JavaDoc fb, int offset, int length,
347                 String JavaDoc string, AttributeSet JavaDoc attr) throws BadLocationException JavaDoc {
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 JavaDoc fb,
362                                               int offset, char aChar) throws
363                               BadLocationException JavaDoc {
364         if (aChar == getMinusSign() || aChar == getPositiveSign()) {
365             NumberFormat.Field field = getFieldFrom(offset, -1);
366             Object JavaDoc 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 JavaDoc 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      * Returns true if the range offset to length identifies the only
399      * integer field.
400      */

401     private boolean isOnlyIntegerField(int offset, int length) {
402         if (isValidMask()) {
403             int start = getAttributeStart(NumberFormat.Field.INTEGER);
404
405             if (start != -1) {
406                 AttributedCharacterIterator iterator = getIterator();
407
408                 iterator.setIndex(start);
409                 if (offset > start || iterator.getRunLimit(
410                     NumberFormat.Field.INTEGER) > (offset + length)) {
411                     return false;
412                 }
413                 return true;
414             }
415         }
416         return false;
417     }
418
419     /**
420      * Invoked to toggle the sign. For this to work the value class
421      * must have a single arg constructor that takes a String.
422      */

423     private Object JavaDoc toggleSign(boolean positive) throws ParseException {
424         Object JavaDoc value = stringToValue(getFormattedTextField().getText());
425
426         if (value != null) {
427             // toString isn't localized, so that using +/- should work
428
// correctly.
429
String JavaDoc string = value.toString();
430
431             if (string != null && string.length() > 0) {
432                 if (positive) {
433                     if (string.charAt(0) == '-') {
434                         string = string.substring(1);
435                     }
436                 }
437                 else {
438                     if (string.charAt(0) == '+') {
439                         string = string.substring(1);
440                     }
441                     if (string.length() > 0 && string.charAt(0) != '-') {
442                         string = "-" + string;
443                     }
444                 }
445                 if (string != null) {
446                     Class JavaDoc valueClass = getValueClass();
447
448                     if (valueClass == null) {
449                         valueClass = value.getClass();
450                     }
451                     try {
452                         Constructor cons = valueClass.getConstructor(
453                                               new Class JavaDoc[] { String JavaDoc.class });
454
455                         if (cons != null) {
456                             return cons.newInstance(new Object JavaDoc[]{string});
457                         }
458                     } catch (Throwable JavaDoc ex) { }
459                 }
460             }
461         }
462         return null;
463     }
464
465     /**
466      * Invoked to toggle the sign of the exponent (for scientific
467      * numbers).
468      */

469     private Object JavaDoc toggleExponentSign(int offset, char aChar) throws
470                              BadLocationException JavaDoc, ParseException {
471         String JavaDoc string = getFormattedTextField().getText();
472         int replaceLength = 0;
473         int loc = getAttributeStart(NumberFormat.Field.EXPONENT_SIGN);
474
475         if (loc >= 0) {
476             replaceLength = 1;
477             offset = loc;
478         }
479         if (aChar == getPositiveSign()) {
480             string = getReplaceString(offset, replaceLength, null);
481         }
482         else {
483             string = getReplaceString(offset, replaceLength,
484                                       new String JavaDoc(new char[] { aChar }));
485         }
486         return stringToValue(string);
487     }
488 }
489
Popular Tags