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>Integer.valueOf(((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 } 195 else if (valueClass == Short.class) { 196 return Short.valueOf(numberValue.shortValue()); 197 } 198 } 199 return value; 200 } 201 202 /** 203 * Returns the character that is used to toggle to positive values. 204 */ 205 private char getPositiveSign() { 206 return '+'; 207 } 208 209 /** 210 * Returns the character that is used to toggle to negative values. 211 */ 212 private char getMinusSign() { 213 DecimalFormatSymbols dfs = getDecimalFormatSymbols(); 214 215 if (dfs != null) { 216 return dfs.getMinusSign(); 217 } 218 return '-'; 219 } 220 221 /** 222 * Returns the character that is used to toggle to negative values. 223 */ 224 private char getDecimalSeparator() { 225 DecimalFormatSymbols dfs = getDecimalFormatSymbols(); 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. 269 */ 270 boolean isLiteral(Map<?, ?> attrs) { 271 if (!super.isLiteral(attrs)) { 272 if (attrs == null) { 273 return false; 274 } 275 int size = attrs.size(); 276 277 if (attrs.get(NumberFormat.Field.GROUPING_SEPARATOR) != null) { 278 size--; 279 if (attrs.get(NumberFormat.Field.INTEGER) != null) { 280 size--; 281 } 282 } 283 if (attrs.get(NumberFormat.Field.EXPONENT_SYMBOL) != null) { 284 size--; 285 } 286 if (attrs.get(NumberFormat.Field.PERCENT) != null) { 287 size--; 288 } 289 if (attrs.get(NumberFormat.Field.PERMILLE) != null) { 290 size--; 291 } 292 if (attrs.get(NumberFormat.Field.CURRENCY) != null) { 293 size--; 294 } 295 if (attrs.get(NumberFormat.Field.SIGN) != null) { 296 size--; 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 } 339 } 340 index += direction; 341 } 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 } 383 if (newValue != null && isValidValue(newValue, false)) { 384 int lc = getLiteralCountTo(offset); 385 String string = valueToString(newValue); 386 387 fb.remove(0, fb.getDocument().getLength()); 388 fb.insertString(0, string, null); 389 updateValue(newValue); 390 repositionCursor(getLiteralCountTo(offset) - 391 lc + offset, 1); 392 return true; 393 } 394 } catch (ParseException pe) { 395 invalidEdit(); 396 } 397 } 398 return false; 399 } 400 401 /** 402 * Invoked to toggle the sign. For this to work the value class 403 * must have a single arg constructor that takes a String. 404 */ 405 private Object toggleSign(boolean positive) throws ParseException { 406 Object value = stringToValue(getFormattedTextField().getText()); 407 408 if (value != null) { 409 // toString isn't localized, so that using +/- should work 410 // correctly. 411 String string = value.toString(); 412 413 if (string != null && string.length() > 0) { 414 if (positive) { 415 if (string.charAt(0) == '-') { 416 string = string.substring(1); 417 } 418 } 419 else { 420 if (string.charAt(0) == '+') { 421 string = string.substring(1); 422 } 423 if (string.length() > 0 && string.charAt(0) != '-') { 424 string = "-" + string; 425 } 426 } 427 if (string != null) { 428 Class<?> valueClass = getValueClass(); 429 430 if (valueClass == null) { 431 valueClass = value.getClass(); 432 } 433 try { 434 ReflectUtil.checkPackageAccess(valueClass); 435 SwingUtilities2.checkAccess(valueClass.getModifiers()); 436 Constructor<?> cons = valueClass.getConstructor( 437 new Class<?>[] { String.class }); 438 if (cons != null) { 439 SwingUtilities2.checkAccess(cons.getModifiers()); 440 return cons.newInstance(new Object[]{string}); 441 } 442 } catch (Throwable ex) { } 443 } 444 } 445 } 446 return null; 447 } 448 449 /** 450 * Invoked to toggle the sign of the exponent (for scientific 451 * numbers). 452 */ 453 private Object toggleExponentSign(int offset, char aChar) throws 454 BadLocationException, ParseException { 455 String string = getFormattedTextField().getText(); 456 int replaceLength = 0; 457 int loc = getAttributeStart(NumberFormat.Field.EXPONENT_SIGN); 458 459 if (loc >= 0) { 460 replaceLength = 1; 461 offset = loc; 462 } 463 if (aChar == getPositiveSign()) { 464 string = getReplaceString(offset, replaceLength, null); 465 } 466 else { 467 string = getReplaceString(offset, replaceLength, 468 new String(new char[] { aChar })); 469 } 470 return stringToValue(string); 471 } 472 }