1 /* 2 * Copyright (c) 2000, 2014, 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™ 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, Class valueClass) { 177 if (valueClass != null && (value instanceof Number)) { 178 Number numberValue = (Number)value; 179 if (valueClass == Integer.class) { 180 return Integer.valueOf(numberValue.intValue()); 181 } 182 else if (valueClass == Long.class) { 183 return Long.valueOf(numberValue.longValue()); 184 } 185 else if (valueClass == Float.class) { 186 return Float.valueOf(numberValue.floatValue()); 187 } 188 else if (valueClass == Double.class) { 189 return Double.valueOf(numberValue.doubleValue()); 190 } 191 else if (valueClass == Byte.class) { 192 return Byte.valueOf(numberValue.byteValue()); 193 } 194 else if (valueClass == Short.class) { 195 return Short.valueOf(numberValue.shortValue()); 196 } 197 } 198 return value; 199 } 200 201 /** 202 * Returns the character that is used to toggle to positive values. 203 */ 204 private char getPositiveSign() { 205 return '+'; 206 } 207 208 /** 209 * Returns the character that is used to toggle to negative values. 210 */ 211 private char getMinusSign() { 212 DecimalFormatSymbols dfs = getDecimalFormatSymbols(); 213 214 if (dfs != null) { 215 return dfs.getMinusSign(); 216 } 217 return '-'; 218 } 219 220 /** 221 * Returns the character that is used to toggle to negative values. 222 */ 223 private char getDecimalSeparator() { 224 DecimalFormatSymbols dfs = getDecimalFormatSymbols(); 225 226 if (dfs != null) { 227 return dfs.getDecimalSeparator(); 228 } 229 return '.'; 230 } 231 232 /** 233 * Returns the DecimalFormatSymbols from the Format instance. 234 */ 235 private DecimalFormatSymbols getDecimalFormatSymbols() { 236 Format f = getFormat(); 237 238 if (f instanceof DecimalFormat) { 239 return ((DecimalFormat)f).getDecimalFormatSymbols(); 240 } 241 return null; 242 } 243 244 /** 245 * Subclassed to return false if <code>text</code> contains in an invalid 246 * character to insert, that is, it is not a digit 247 * (<code>Character.isDigit()</code>) and 248 * not one of the characters defined by the DecimalFormatSymbols. 249 */ 250 boolean isLegalInsertText(String text) { 251 if (getAllowsInvalid()) { 252 return true; 253 } 254 for (int counter = text.length() - 1; counter >= 0; counter--) { 255 char aChar = text.charAt(counter); 256 257 if (!Character.isDigit(aChar) && 258 specialChars.indexOf(aChar) == -1){ 259 return false; 260 } 261 } 262 return true; 263 } 264 265 /** 266 * Subclassed to treat the decimal separator, grouping separator, 267 * exponent symbol, percent, permille, currency and sign as literals. 268 */ 269 boolean isLiteral(Map attrs) { 270 if (!super.isLiteral(attrs)) { 271 if (attrs == null) { 272 return false; 273 } 274 int size = attrs.size(); 275 276 if (attrs.get(NumberFormat.Field.GROUPING_SEPARATOR) != null) { 277 size--; 278 if (attrs.get(NumberFormat.Field.INTEGER) != null) { 279 size--; 280 } 281 } 282 if (attrs.get(NumberFormat.Field.EXPONENT_SYMBOL) != null) { 283 size--; 284 } 285 if (attrs.get(NumberFormat.Field.PERCENT) != null) { 286 size--; 287 } 288 if (attrs.get(NumberFormat.Field.PERMILLE) != null) { 289 size--; 290 } 291 if (attrs.get(NumberFormat.Field.CURRENCY) != null) { 292 size--; 293 } 294 if (attrs.get(NumberFormat.Field.SIGN) != null) { 295 size--; 296 } 297 return size == 0; 298 } 299 return true; 300 } 301 302 /** 303 * Subclassed to make the decimal separator navigable, as well 304 * as making the character between the integer field and the next 305 * field navigable. 306 */ 307 boolean isNavigatable(int index) { 308 if (!super.isNavigatable(index)) { 309 // Don't skip the decimal, it causes wierd behavior 310 return getBufferedChar(index) == getDecimalSeparator(); 311 } 312 return true; 313 } 314 315 /** 316 * Returns the first <code>NumberFormat.Field</code> starting 317 * <code>index</code> incrementing by <code>direction</code>. 318 */ 319 private NumberFormat.Field getFieldFrom(int index, int direction) { 320 if (isValidMask()) { 321 int max = getFormattedTextField().getDocument().getLength(); 322 AttributedCharacterIterator iterator = getIterator(); 323 324 if (index >= max) { 325 index += direction; 326 } 327 while (index >= 0 && index < max) { 328 iterator.setIndex(index); 329 330 Map attrs = iterator.getAttributes(); 331 332 if (attrs != null && attrs.size() > 0) { 333 for (Object key : attrs.keySet()) { 334 if (key instanceof NumberFormat.Field) { 335 return (NumberFormat.Field)key; 336 } 337 } 338 } 339 index += direction; 340 } 341 } 342 return null; 343 } 344 345 /** 346 * Overriden to toggle the value if the positive/minus sign 347 * is inserted. 348 */ 349 void replace(DocumentFilter.FilterBypass fb, int offset, int length, 350 String string, AttributeSet attr) throws BadLocationException { 351 if (!getAllowsInvalid() && length == 0 && string != null && 352 string.length() == 1 && 353 toggleSignIfNecessary(fb, offset, string.charAt(0))) { 354 return; 355 } 356 super.replace(fb, offset, length, string, attr); 357 } 358 359 /** 360 * Will change the sign of the integer or exponent field if 361 * <code>aChar</code> is the positive or minus sign. Returns 362 * true if a sign change was attempted. 363 */ 364 private boolean toggleSignIfNecessary(DocumentFilter.FilterBypass fb, 365 int offset, char aChar) throws 366 BadLocationException { 367 if (aChar == getMinusSign() || aChar == getPositiveSign()) { 368 NumberFormat.Field field = getFieldFrom(offset, -1); 369 Object newValue; 370 371 try { 372 if (field == null || 373 (field != NumberFormat.Field.EXPONENT && 374 field != NumberFormat.Field.EXPONENT_SYMBOL && 375 field != NumberFormat.Field.EXPONENT_SIGN)) { 376 newValue = toggleSign((aChar == getPositiveSign())); 377 } 378 else { 379 // exponent 380 newValue = toggleExponentSign(offset, aChar); 381 } 382 if (newValue != null && isValidValue(newValue, false)) { 383 int lc = getLiteralCountTo(offset); 384 String string = valueToString(newValue); 385 386 fb.remove(0, fb.getDocument().getLength()); 387 fb.insertString(0, string, null); 388 updateValue(newValue); 389 repositionCursor(getLiteralCountTo(offset) - 390 lc + offset, 1); 391 return true; 392 } 393 } catch (ParseException pe) { 394 invalidEdit(); 395 } 396 } 397 return false; 398 } 399 400 /** 401 * Invoked to toggle the sign. For this to work the value class 402 * must have a single arg constructor that takes a String. 403 */ 404 private Object toggleSign(boolean positive) throws ParseException { 405 Object value = stringToValue(getFormattedTextField().getText()); 406 407 if (value != null) { 408 // toString isn't localized, so that using +/- should work 409 // correctly. 410 String string = value.toString(); 411 412 if (string != null && string.length() > 0) { 413 if (positive) { 414 if (string.charAt(0) == '-') { 415 string = string.substring(1); 416 } 417 } 418 else { 419 if (string.charAt(0) == '+') { 420 string = string.substring(1); 421 } 422 if (string.length() > 0 && string.charAt(0) != '-') { 423 string = "-" + string; 424 } 425 } 426 if (string != null) { 427 Class<?> valueClass = getValueClass(); 428 429 if (valueClass == null) { 430 valueClass = value.getClass(); 431 } 432 try { 433 ReflectUtil.checkPackageAccess(valueClass); 434 SwingUtilities2.checkAccess(valueClass.getModifiers()); 435 Constructor cons = valueClass.getConstructor( 436 new Class[] { String.class }); 437 if (cons != null) { 438 SwingUtilities2.checkAccess(cons.getModifiers()); 439 return cons.newInstance(new Object[]{string}); 440 } 441 } catch (Throwable ex) { } 442 } 443 } 444 } 445 return null; 446 } 447 448 /** 449 * Invoked to toggle the sign of the exponent (for scientific 450 * numbers). 451 */ 452 private Object toggleExponentSign(int offset, char aChar) throws 453 BadLocationException, ParseException { 454 String string = getFormattedTextField().getText(); 455 int replaceLength = 0; 456 int loc = getAttributeStart(NumberFormat.Field.EXPONENT_SIGN); 457 458 if (loc >= 0) { 459 replaceLength = 1; 460 offset = loc; 461 } 462 if (aChar == getPositiveSign()) { 463 string = getReplaceString(offset, replaceLength, null); 464 } 465 else { 466 string = getReplaceString(offset, replaceLength, 467 new String(new char[] { aChar })); 468 } 469 return stringToValue(string); 470 } 471 }