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™
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 @SuppressWarnings("serial") // Same-version serialization only
95 public class NumberFormatter extends InternationalFormatter {
96 /** The special characters from the Format instance. */
97 private String specialChars;
98
99 /**
100 * Creates a <code>NumberFormatter</code> with the a default
101 * <code>NumberFormat</code> instance obtained from
102 * <code>NumberFormat.getNumberInstance()</code>.
103 */
104 public NumberFormatter() {
105 this(NumberFormat.getNumberInstance());
106 }
107
108 /**
109 * Creates a NumberFormatter with the specified Format instance.
110 *
111 * @param format Format used to dictate legal values
112 */
113 public NumberFormatter(NumberFormat format) {
114 super(format);
115 setFormat(format);
116 setAllowsInvalid(true);
117 setCommitsOnValidEdit(false);
118 setOverwriteMode(false);
119 }
120
121 /**
122 * Sets the format that dictates the legal values that can be edited
123 * and displayed.
124 * <p>
125 * If you have used the nullary constructor the value of this property
126 * will be determined for the current locale by way of the
127 * <code>NumberFormat.getNumberInstance()</code> method.
128 *
129 * @param format NumberFormat instance used to dictate legal values
130 */
131 public void setFormat(Format format) {
132 super.setFormat(format);
133
134 DecimalFormatSymbols dfs = getDecimalFormatSymbols();
135
136 if (dfs != null) {
137 StringBuilder sb = new StringBuilder();
138
139 sb.append(dfs.getCurrencySymbol());
140 sb.append(dfs.getDecimalSeparator());
141 sb.append(dfs.getGroupingSeparator());
142 sb.append(dfs.getInfinity());
143 sb.append(dfs.getInternationalCurrencySymbol());
144 sb.append(dfs.getMinusSign());
145 sb.append(dfs.getMonetaryDecimalSeparator());
146 sb.append(dfs.getNaN());
147 sb.append(dfs.getPercent());
148 sb.append('+');
149 specialChars = sb.toString();
150 }
151 else {
152 specialChars = "";
153 }
154 }
155
156 /**
157 * Invokes <code>parseObject</code> on <code>f</code>, returning
158 * its value.
159 */
160 Object stringToValue(String text, Format f) throws ParseException {
161 if (f == null) {
162 return text;
163 }
164 Object value = f.parseObject(text);
165
166 return convertValueToValueClass(value, getValueClass());
167 }
168
169 /**
170 * Converts the passed in value to the passed in class. This only
171 * works if <code>valueClass</code> is one of <code>Integer</code>,
172 * <code>Long</code>, <code>Float</code>, <code>Double</code>,
173 * <code>Byte</code> or <code>Short</code> and <code>value</code>
174 * is an instanceof <code>Number</code>.
175 */
176 private Object convertValueToValueClass(Object value,
177 Class<?> valueClass) {
178 if (valueClass != null && (value instanceof Number)) {
179 Number numberValue = (Number)value;
180 if (valueClass == Integer.class) {
181 return Integer.valueOf(numberValue.intValue());
182 }
183 else if (valueClass == Long.class) {
184 return Long.valueOf(numberValue.longValue());
185 }
186 else if (valueClass == Float.class) {
187 return Float.valueOf(numberValue.floatValue());
188 }
189 else if (valueClass == Double.class) {
190 return Double.valueOf(numberValue.doubleValue());
191 }
192 else if (valueClass == Byte.class) {
193 return Byte.valueOf(numberValue.byteValue());
194 }
226
227 if (dfs != null) {
228 return dfs.getDecimalSeparator();
229 }
230 return '.';
231 }
232
233 /**
234 * Returns the DecimalFormatSymbols from the Format instance.
235 */
236 private DecimalFormatSymbols getDecimalFormatSymbols() {
237 Format f = getFormat();
238
239 if (f instanceof DecimalFormat) {
240 return ((DecimalFormat)f).getDecimalFormatSymbols();
241 }
242 return null;
243 }
244
245 /**
246 * Subclassed to return false if <code>text</code> contains in an invalid
247 * character to insert, that is, it is not a digit
248 * (<code>Character.isDigit()</code>) and
249 * not one of the characters defined by the DecimalFormatSymbols.
250 */
251 boolean isLegalInsertText(String text) {
252 if (getAllowsInvalid()) {
253 return true;
254 }
255 for (int counter = text.length() - 1; counter >= 0; counter--) {
256 char aChar = text.charAt(counter);
257
258 if (!Character.isDigit(aChar) &&
259 specialChars.indexOf(aChar) == -1){
260 return false;
261 }
262 }
263 return true;
264 }
265
266 /**
267 * Subclassed to treat the decimal separator, grouping separator,
268 * exponent symbol, percent, permille, currency and sign as literals.
297 }
298 return size == 0;
299 }
300 return true;
301 }
302
303 /**
304 * Subclassed to make the decimal separator navigable, as well
305 * as making the character between the integer field and the next
306 * field navigable.
307 */
308 boolean isNavigatable(int index) {
309 if (!super.isNavigatable(index)) {
310 // Don't skip the decimal, it causes wierd behavior
311 return getBufferedChar(index) == getDecimalSeparator();
312 }
313 return true;
314 }
315
316 /**
317 * Returns the first <code>NumberFormat.Field</code> starting
318 * <code>index</code> incrementing by <code>direction</code>.
319 */
320 private NumberFormat.Field getFieldFrom(int index, int direction) {
321 if (isValidMask()) {
322 int max = getFormattedTextField().getDocument().getLength();
323 AttributedCharacterIterator iterator = getIterator();
324
325 if (index >= max) {
326 index += direction;
327 }
328 while (index >= 0 && index < max) {
329 iterator.setIndex(index);
330
331 Map<?,?> attrs = iterator.getAttributes();
332
333 if (attrs != null && attrs.size() > 0) {
334 for (Object key : attrs.keySet()) {
335 if (key instanceof NumberFormat.Field) {
336 return (NumberFormat.Field)key;
337 }
338 }
342 }
343 return null;
344 }
345
346 /**
347 * Overriden to toggle the value if the positive/minus sign
348 * is inserted.
349 */
350 void replace(DocumentFilter.FilterBypass fb, int offset, int length,
351 String string, AttributeSet attr) throws BadLocationException {
352 if (!getAllowsInvalid() && length == 0 && string != null &&
353 string.length() == 1 &&
354 toggleSignIfNecessary(fb, offset, string.charAt(0))) {
355 return;
356 }
357 super.replace(fb, offset, length, string, attr);
358 }
359
360 /**
361 * Will change the sign of the integer or exponent field if
362 * <code>aChar</code> is the positive or minus sign. Returns
363 * true if a sign change was attempted.
364 */
365 private boolean toggleSignIfNecessary(DocumentFilter.FilterBypass fb,
366 int offset, char aChar) throws
367 BadLocationException {
368 if (aChar == getMinusSign() || aChar == getPositiveSign()) {
369 NumberFormat.Field field = getFieldFrom(offset, -1);
370 Object newValue;
371
372 try {
373 if (field == null ||
374 (field != NumberFormat.Field.EXPONENT &&
375 field != NumberFormat.Field.EXPONENT_SYMBOL &&
376 field != NumberFormat.Field.EXPONENT_SIGN)) {
377 newValue = toggleSign((aChar == getPositiveSign()));
378 }
379 else {
380 // exponent
381 newValue = toggleExponentSign(offset, aChar);
382 }
|
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} subclasses {@code InternationalFormatter}
35 * adding special behavior for numbers. Among the specializations are
36 * (these are only used if the {@code NumberFormatter} does not display
37 * invalid numbers, for example, {@code setAllowsInvalid(false)}):
38 * <ul>
39 * <li>Pressing +/- (- is determined from the
40 * {@code DecimalFormatSymbols} associated with the
41 * {@code DecimalFormat}) 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} associated with the
46 * {@code DecimalFormat}) 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)}. 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###}), or allow the value to be invalid
62 * {@code setAllowsInvalid(true)}. Otherwise users may not be able to
63 * input decimal values.
64 * <p>
65 * {@code NumberFormatter} provides slightly different behavior to
66 * {@code stringToValue} 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 Long}, {@code Float},
69 * {@code Double}, {@code Byte} or {@code Short} and
70 * the Format's {@code parseObject} returns an instance of
71 * {@code Number}, 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)} will cause the resulting
75 * value to be created via
76 * {@code new Integer(((Number)formatter.parseObject(string)).intValue())}.
77 * This is typically useful if you
78 * wish to set a min/max value as the various {@code Number}
79 * implementations are generally not comparable to each other. This is also
80 * useful if for some reason you need a specific {@code Number}
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™
89 * has been added to the {@code java.beans} package.
90 * Please see {@link java.beans.XMLEncoder}.
91 *
92 * @since 1.4
93 */
94 @SuppressWarnings("serial") // Same-version serialization only
95 public class NumberFormatter extends InternationalFormatter {
96 /** The special characters from the Format instance. */
97 private String specialChars;
98
99 /**
100 * Creates a {@code NumberFormatter} with the a default
101 * {@code NumberFormat} instance obtained from
102 * {@code NumberFormat.getNumberInstance()}.
103 */
104 public NumberFormatter() {
105 this(NumberFormat.getNumberInstance());
106 }
107
108 /**
109 * Creates a NumberFormatter with the specified Format instance.
110 *
111 * @param format Format used to dictate legal values
112 */
113 public NumberFormatter(NumberFormat format) {
114 super(format);
115 setFormat(format);
116 setAllowsInvalid(true);
117 setCommitsOnValidEdit(false);
118 setOverwriteMode(false);
119 }
120
121 /**
122 * Sets the format that dictates the legal values that can be edited
123 * and displayed.
124 * <p>
125 * If you have used the nullary constructor the value of this property
126 * will be determined for the current locale by way of the
127 * {@code NumberFormat.getNumberInstance()} method.
128 *
129 * @param format NumberFormat instance used to dictate legal values
130 */
131 public void setFormat(Format format) {
132 super.setFormat(format);
133
134 DecimalFormatSymbols dfs = getDecimalFormatSymbols();
135
136 if (dfs != null) {
137 StringBuilder sb = new StringBuilder();
138
139 sb.append(dfs.getCurrencySymbol());
140 sb.append(dfs.getDecimalSeparator());
141 sb.append(dfs.getGroupingSeparator());
142 sb.append(dfs.getInfinity());
143 sb.append(dfs.getInternationalCurrencySymbol());
144 sb.append(dfs.getMinusSign());
145 sb.append(dfs.getMonetaryDecimalSeparator());
146 sb.append(dfs.getNaN());
147 sb.append(dfs.getPercent());
148 sb.append('+');
149 specialChars = sb.toString();
150 }
151 else {
152 specialChars = "";
153 }
154 }
155
156 /**
157 * Invokes {@code parseObject} on {@code f}, returning
158 * its value.
159 */
160 Object stringToValue(String text, Format f) throws ParseException {
161 if (f == null) {
162 return text;
163 }
164 Object value = f.parseObject(text);
165
166 return convertValueToValueClass(value, getValueClass());
167 }
168
169 /**
170 * Converts the passed in value to the passed in class. This only
171 * works if {@code valueClass} is one of {@code Integer},
172 * {@code Long}, {@code Float}, {@code Double},
173 * {@code Byte} or {@code Short} and {@code value}
174 * is an instanceof {@code Number}.
175 */
176 private Object convertValueToValueClass(Object value,
177 Class<?> valueClass) {
178 if (valueClass != null && (value instanceof Number)) {
179 Number numberValue = (Number)value;
180 if (valueClass == Integer.class) {
181 return Integer.valueOf(numberValue.intValue());
182 }
183 else if (valueClass == Long.class) {
184 return Long.valueOf(numberValue.longValue());
185 }
186 else if (valueClass == Float.class) {
187 return Float.valueOf(numberValue.floatValue());
188 }
189 else if (valueClass == Double.class) {
190 return Double.valueOf(numberValue.doubleValue());
191 }
192 else if (valueClass == Byte.class) {
193 return Byte.valueOf(numberValue.byteValue());
194 }
226
227 if (dfs != null) {
228 return dfs.getDecimalSeparator();
229 }
230 return '.';
231 }
232
233 /**
234 * Returns the DecimalFormatSymbols from the Format instance.
235 */
236 private DecimalFormatSymbols getDecimalFormatSymbols() {
237 Format f = getFormat();
238
239 if (f instanceof DecimalFormat) {
240 return ((DecimalFormat)f).getDecimalFormatSymbols();
241 }
242 return null;
243 }
244
245 /**
246 * Subclassed to return false if {@code text} contains in an invalid
247 * character to insert, that is, it is not a digit
248 * ({@code Character.isDigit()}) and
249 * not one of the characters defined by the DecimalFormatSymbols.
250 */
251 boolean isLegalInsertText(String text) {
252 if (getAllowsInvalid()) {
253 return true;
254 }
255 for (int counter = text.length() - 1; counter >= 0; counter--) {
256 char aChar = text.charAt(counter);
257
258 if (!Character.isDigit(aChar) &&
259 specialChars.indexOf(aChar) == -1){
260 return false;
261 }
262 }
263 return true;
264 }
265
266 /**
267 * Subclassed to treat the decimal separator, grouping separator,
268 * exponent symbol, percent, permille, currency and sign as literals.
297 }
298 return size == 0;
299 }
300 return true;
301 }
302
303 /**
304 * Subclassed to make the decimal separator navigable, as well
305 * as making the character between the integer field and the next
306 * field navigable.
307 */
308 boolean isNavigatable(int index) {
309 if (!super.isNavigatable(index)) {
310 // Don't skip the decimal, it causes wierd behavior
311 return getBufferedChar(index) == getDecimalSeparator();
312 }
313 return true;
314 }
315
316 /**
317 * Returns the first {@code NumberFormat.Field} starting
318 * {@code index} incrementing by {@code direction}.
319 */
320 private NumberFormat.Field getFieldFrom(int index, int direction) {
321 if (isValidMask()) {
322 int max = getFormattedTextField().getDocument().getLength();
323 AttributedCharacterIterator iterator = getIterator();
324
325 if (index >= max) {
326 index += direction;
327 }
328 while (index >= 0 && index < max) {
329 iterator.setIndex(index);
330
331 Map<?,?> attrs = iterator.getAttributes();
332
333 if (attrs != null && attrs.size() > 0) {
334 for (Object key : attrs.keySet()) {
335 if (key instanceof NumberFormat.Field) {
336 return (NumberFormat.Field)key;
337 }
338 }
342 }
343 return null;
344 }
345
346 /**
347 * Overriden to toggle the value if the positive/minus sign
348 * is inserted.
349 */
350 void replace(DocumentFilter.FilterBypass fb, int offset, int length,
351 String string, AttributeSet attr) throws BadLocationException {
352 if (!getAllowsInvalid() && length == 0 && string != null &&
353 string.length() == 1 &&
354 toggleSignIfNecessary(fb, offset, string.charAt(0))) {
355 return;
356 }
357 super.replace(fb, offset, length, string, attr);
358 }
359
360 /**
361 * Will change the sign of the integer or exponent field if
362 * {@code aChar} is the positive or minus sign. Returns
363 * true if a sign change was attempted.
364 */
365 private boolean toggleSignIfNecessary(DocumentFilter.FilterBypass fb,
366 int offset, char aChar) throws
367 BadLocationException {
368 if (aChar == getMinusSign() || aChar == getPositiveSign()) {
369 NumberFormat.Field field = getFieldFrom(offset, -1);
370 Object newValue;
371
372 try {
373 if (field == null ||
374 (field != NumberFormat.Field.EXPONENT &&
375 field != NumberFormat.Field.EXPONENT_SYMBOL &&
376 field != NumberFormat.Field.EXPONENT_SIGN)) {
377 newValue = toggleSign((aChar == getPositiveSign()));
378 }
379 else {
380 // exponent
381 newValue = toggleExponentSign(offset, aChar);
382 }
|