001 /*
002 * Copyright 2000-2003 Sun Microsystems, Inc. All Rights Reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation. Sun designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Sun in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
022 * CA 95054 USA or visit www.sun.com if you need additional information or
023 * have any questions.
024 */
025 package javax.swing.text;
026
027 import java.lang.reflect.*;
028 import java.text.*;
029 import java.util.*;
030 import javax.swing.text.*;
031
032 /**
033 * <code>NumberFormatter</code> subclasses <code>InternationalFormatter</code>
034 * adding special behavior for numbers. Among the specializations are
035 * (these are only used if the <code>NumberFormatter</code> does not display
036 * invalid nubers, eg <code>setAllowsInvalid(false)</code>):
037 * <ul>
038 * <li>Pressing +/- (- is determined from the
039 * <code>DecimalFormatSymbols</code> associated with the
040 * <code>DecimalFormat</code>) in any field but the exponent
041 * field will attempt to change the sign of the number to
042 * positive/negative.
043 * <li>Pressing +/- (- is determined from the
044 * <code>DecimalFormatSymbols</code> associated with the
045 * <code>DecimalFormat</code>) in the exponent field will
046 * attemp to change the sign of the exponent to positive/negative.
047 * </ul>
048 * <p>
049 * If you are displaying scientific numbers, you may wish to turn on
050 * overwrite mode, <code>setOverwriteMode(true)</code>. For example:
051 * <pre>
052 * DecimalFormat decimalFormat = new DecimalFormat("0.000E0");
053 * NumberFormatter textFormatter = new NumberFormatter(decimalFormat);
054 * textFormatter.setOverwriteMode(true);
055 * textFormatter.setAllowsInvalid(false);
056 * </pre>
057 * <p>
058 * If you are going to allow the user to enter decimal
059 * values, you should either force the DecimalFormat to contain at least
060 * one decimal (<code>#.0###</code>), or allow the value to be invalid
061 * <code>setAllowsInvalid(true)</code>. Otherwise users may not be able to
062 * input decimal values.
063 * <p>
064 * <code>NumberFormatter</code> provides slightly different behavior to
065 * <code>stringToValue</code> than that of its superclass. If you have
066 * specified a Class for values, {@link #setValueClass}, that is one of
067 * of <code>Integer</code>, <code>Long</code>, <code>Float</code>,
068 * <code>Double</code>, <code>Byte</code> or <code>Short</code> and
069 * the Format's <code>parseObject</code> returns an instance of
070 * <code>Number</code>, the corresponding instance of the value class
071 * will be created using the constructor appropriate for the primitive
072 * type the value class represents. For example:
073 * <code>setValueClass(Integer.class)</code> will cause the resulting
074 * value to be created via
075 * <code>new Integer(((Number)formatter.parseObject(string)).intValue())</code>.
076 * This is typically useful if you
077 * wish to set a min/max value as the various <code>Number</code>
078 * implementations are generally not comparable to each other. This is also
079 * useful if for some reason you need a specific <code>Number</code>
080 * implementation for your values.
081 * <p>
082 * <strong>Warning:</strong>
083 * Serialized objects of this class will not be compatible with
084 * future Swing releases. The current serialization support is
085 * appropriate for short term storage or RMI between applications running
086 * the same version of Swing. As of 1.4, support for long term storage
087 * of all JavaBeans<sup><font size="-2">TM</font></sup>
088 * has been added to the <code>java.beans</code> package.
089 * Please see {@link java.beans.XMLEncoder}.
090 *
091 * @version 1.4 03/05/01
092 * @since 1.4
093 */
094 public class NumberFormatter extends InternationalFormatter {
095 /** The special characters from the Format instance. */
096 private String specialChars;
097
098 /**
099 * 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 StringBuffer sb = new StringBuffer();
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 } else {
150 specialChars = "";
151 }
152 }
153
154 /**
155 * Invokes <code>parseObject</code> on <code>f</code>, returning
156 * its value.
157 */
158 Object stringToValue(String text, Format f) throws ParseException {
159 if (f == null) {
160 return text;
161 }
162 Object value = f.parseObject(text);
163
164 return convertValueToValueClass(value, getValueClass());
165 }
166
167 /**
168 * Converts the passed in value to the passed in class. This only
169 * works if <code>valueClass</code> is one of <code>Integer</code>,
170 * <code>Long</code>, <code>Float</code>, <code>Double</code>,
171 * <code>Byte</code> or <code>Short</code> and <code>value</code>
172 * is an instanceof <code>Number</code>.
173 */
174 private Object convertValueToValueClass(Object value,
175 Class valueClass) {
176 if (valueClass != null && (value instanceof Number)) {
177 if (valueClass == Integer.class) {
178 return new Integer(((Number) value).intValue());
179 } else if (valueClass == Long.class) {
180 return new Long(((Number) value).longValue());
181 } else if (valueClass == Float.class) {
182 return new Float(((Number) value).floatValue());
183 } else if (valueClass == Double.class) {
184 return new Double(((Number) value).doubleValue());
185 } else if (valueClass == Byte.class) {
186 return new Byte(((Number) value).byteValue());
187 } else if (valueClass == Short.class) {
188 return new Short(((Number) value).shortValue());
189 }
190 }
191 return value;
192 }
193
194 /**
195 * Returns the character that is used to toggle to positive values.
196 */
197 private char getPositiveSign() {
198 return '+';
199 }
200
201 /**
202 * Returns the character that is used to toggle to negative values.
203 */
204 private char getMinusSign() {
205 DecimalFormatSymbols dfs = getDecimalFormatSymbols();
206
207 if (dfs != null) {
208 return dfs.getMinusSign();
209 }
210 return '-';
211 }
212
213 /**
214 * Returns the character that is used to toggle to negative values.
215 */
216 private char getDecimalSeparator() {
217 DecimalFormatSymbols dfs = getDecimalFormatSymbols();
218
219 if (dfs != null) {
220 return dfs.getDecimalSeparator();
221 }
222 return '.';
223 }
224
225 /**
226 * Returns the DecimalFormatSymbols from the Format instance.
227 */
228 private DecimalFormatSymbols getDecimalFormatSymbols() {
229 Format f = getFormat();
230
231 if (f instanceof DecimalFormat) {
232 return ((DecimalFormat) f).getDecimalFormatSymbols();
233 }
234 return null;
235 }
236
237 /**
238 */
239 private boolean isValidInsertionCharacter(char aChar) {
240 return (Character.isDigit(aChar) || specialChars.indexOf(aChar) != -1);
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 if (size == 0) {
297 return true;
298 }
299 return false;
300 }
301 return true;
302 }
303
304 /**
305 * Subclassed to make the decimal separator navigatable, as well
306 * as making the character between the integer field and the next
307 * field navigatable.
308 */
309 boolean isNavigatable(int index) {
310 if (!super .isNavigatable(index)) {
311 // Don't skip the decimal, it causes wierd behavior
312 if (getBufferedChar(index) == getDecimalSeparator()) {
313 return true;
314 }
315 return false;
316 }
317 return true;
318 }
319
320 /**
321 * Returns the first <code>NumberFormat.Field</code> starting
322 * <code>index</code> incrementing by <code>direction</code>.
323 */
324 private NumberFormat.Field getFieldFrom(int index, int direction) {
325 if (isValidMask()) {
326 int max = getFormattedTextField().getDocument().getLength();
327 AttributedCharacterIterator iterator = getIterator();
328
329 if (index >= max) {
330 index += direction;
331 }
332 while (index >= 0 && index < max) {
333 iterator.setIndex(index);
334
335 Map attrs = iterator.getAttributes();
336
337 if (attrs != null && attrs.size() > 0) {
338 Iterator keys = attrs.keySet().iterator();
339
340 while (keys.hasNext()) {
341 Object key = keys.next();
342
343 if (key instanceof NumberFormat.Field) {
344 return (NumberFormat.Field) key;
345 }
346 }
347 }
348 index += direction;
349 }
350 }
351 return null;
352 }
353
354 /**
355 * Overriden to toggle the value if the positive/minus sign
356 * is inserted.
357 */
358 void replace(DocumentFilter.FilterBypass fb, int offset,
359 int length, String string, AttributeSet attr)
360 throws BadLocationException {
361 if (!getAllowsInvalid() && length == 0 && string != null
362 && string.length() == 1
363 && toggleSignIfNecessary(fb, offset, string.charAt(0))) {
364 return;
365 }
366 super .replace(fb, offset, length, string, attr);
367 }
368
369 /**
370 * Will change the sign of the integer or exponent field if
371 * <code>aChar</code> is the positive or minus sign. Returns
372 * true if a sign change was attempted.
373 */
374 private boolean toggleSignIfNecessary(
375 DocumentFilter.FilterBypass fb, int offset, char aChar)
376 throws BadLocationException {
377 if (aChar == getMinusSign() || aChar == getPositiveSign()) {
378 NumberFormat.Field field = getFieldFrom(offset, -1);
379 Object newValue;
380
381 try {
382 if (field == null
383 || (field != NumberFormat.Field.EXPONENT
384 && field != NumberFormat.Field.EXPONENT_SYMBOL && field != NumberFormat.Field.EXPONENT_SIGN)) {
385 newValue = toggleSign((aChar == getPositiveSign()));
386 } else {
387 // exponent
388 newValue = toggleExponentSign(offset, aChar);
389 }
390 if (newValue != null && isValidValue(newValue, false)) {
391 int lc = getLiteralCountTo(offset);
392 String string = valueToString(newValue);
393
394 fb.remove(0, fb.getDocument().getLength());
395 fb.insertString(0, string, null);
396 updateValue(newValue);
397 repositionCursor(getLiteralCountTo(offset) - lc
398 + offset, 1);
399 return true;
400 }
401 } catch (ParseException pe) {
402 invalidEdit();
403 }
404 }
405 return false;
406 }
407
408 /**
409 * Returns true if the range offset to length identifies the only
410 * integer field.
411 */
412 private boolean isOnlyIntegerField(int offset, int length) {
413 if (isValidMask()) {
414 int start = getAttributeStart(NumberFormat.Field.INTEGER);
415
416 if (start != -1) {
417 AttributedCharacterIterator iterator = getIterator();
418
419 iterator.setIndex(start);
420 if (offset > start
421 || iterator
422 .getRunLimit(NumberFormat.Field.INTEGER) > (offset + length)) {
423 return false;
424 }
425 return true;
426 }
427 }
428 return false;
429 }
430
431 /**
432 * Invoked to toggle the sign. For this to work the value class
433 * must have a single arg constructor that takes a String.
434 */
435 private Object toggleSign(boolean positive) throws ParseException {
436 Object value = stringToValue(getFormattedTextField().getText());
437
438 if (value != null) {
439 // toString isn't localized, so that using +/- should work
440 // correctly.
441 String string = value.toString();
442
443 if (string != null && string.length() > 0) {
444 if (positive) {
445 if (string.charAt(0) == '-') {
446 string = string.substring(1);
447 }
448 } else {
449 if (string.charAt(0) == '+') {
450 string = string.substring(1);
451 }
452 if (string.length() > 0 && string.charAt(0) != '-') {
453 string = "-" + string;
454 }
455 }
456 if (string != null) {
457 Class valueClass = getValueClass();
458
459 if (valueClass == null) {
460 valueClass = value.getClass();
461 }
462 try {
463 Constructor cons = valueClass
464 .getConstructor(new Class[] { String.class });
465
466 if (cons != null) {
467 return cons
468 .newInstance(new Object[] { string });
469 }
470 } catch (Throwable ex) {
471 }
472 }
473 }
474 }
475 return null;
476 }
477
478 /**
479 * Invoked to toggle the sign of the exponent (for scientific
480 * numbers).
481 */
482 private Object toggleExponentSign(int offset, char aChar)
483 throws BadLocationException, ParseException {
484 String string = getFormattedTextField().getText();
485 int replaceLength = 0;
486 int loc = getAttributeStart(NumberFormat.Field.EXPONENT_SIGN);
487
488 if (loc >= 0) {
489 replaceLength = 1;
490 offset = loc;
491 }
492 if (aChar == getPositiveSign()) {
493 string = getReplaceString(offset, replaceLength, null);
494 } else {
495 string = getReplaceString(offset, replaceLength,
496 new String(new char[] { aChar }));
497 }
498 return stringToValue(string);
499 }
500 }
|