001 package org.springframework.richclient.components; 002 003 import org.apache.commons.logging.Log; 004 import org.apache.commons.logging.LogFactory; 005 import org.springframework.util.Assert; 006 007 import javax.swing.*; 008 import javax.swing.text.AttributeSet; 009 import javax.swing.text.BadLocationException; 010 import javax.swing.text.PlainDocument; 011 import java.awt.event.FocusEvent; 012 import java.awt.event.FocusListener; 013 import java.math.BigDecimal; 014 import java.math.BigInteger; 015 import java.text.DecimalFormat; 016 import java.text.DecimalFormatSymbols; 017 import java.text.NumberFormat; 018 import java.text.ParseException; 019 import java.util.ArrayList; 020 import java.util.Iterator; 021 import java.util.List; 022 023 /** 024 * <p> 025 * This class can have different "read" and "write" formats. When showing the 026 * number the "read" format will be used. If the user enters the inputfield 027 * (gains focus), the "write" format will be used. 028 * </p> 029 * 030 * <p> 031 * A maximum of decimals/non-decimals can be specified so no more numbers can be 032 * input than strictly defined. 033 * </p> 034 * 035 * <p> 036 * A boolean can be specified to allow only positive numbers or positive and 037 * negative numbers. Switching between positive and negative can be done by 038 * using the +/- buttons anywhere in the inputfield. 039 * </p> 040 * 041 * TODO There's a third option: only negative numbers, this should be 042 * configurable as well. 043 * 044 * @author Jan Hoskens 045 * 046 */ 047 public class BigDecimalTextField extends JTextField { 048 049 private static final long serialVersionUID = -601376040393562990L; 050 051 Log log = LogFactory.getLog(BigDecimalTextField.class); 052 053 public static final NumberFormat DEFAULT_FORMAT = new DecimalFormat("###,###,###,##0.######"); 054 055 public static final NumberFormat DEFAULT_UNFORMAT = new DecimalFormat("#0.#######"); 056 057 public static final DecimalFormatSymbols symbols = new DecimalFormatSymbols(); 058 059 private Class numberClass = null; 060 061 private final NumberFormat format; 062 063 private final NumberFormat unformat; 064 065 private Integer scale; 066 067 private List listeners; 068 069 private boolean internallySettingText = false; 070 071 /** 072 * Default constructor. 073 */ 074 public BigDecimalTextField() { 075 this(2, 4, true); 076 } 077 078 /** 079 * @see #BigDecimalTextField(int, int, boolean, Class, NumberFormat, 080 * NumberFormat) 081 */ 082 public BigDecimalTextField(int nrOfNonDecimals, int nrOfDecimals, boolean negativeSign) { 083 this(nrOfNonDecimals, nrOfDecimals, negativeSign, BigDecimal.class); 084 } 085 086 /** 087 * @see #BigDecimalTextField(int, int, boolean, Class, NumberFormat, 088 * NumberFormat) 089 */ 090 public BigDecimalTextField(int nrOfNonDecimals, int nrOfDecimals, boolean negativeSign, Class numberClass) { 091 this(nrOfNonDecimals, nrOfDecimals, negativeSign, numberClass, DEFAULT_FORMAT); 092 } 093 094 /** 095 * @see #BigDecimalTextField(int, int, boolean, Class, NumberFormat, 096 * NumberFormat) 097 */ 098 public BigDecimalTextField(int nrOfNonDecimals, int nrOfDecimals, boolean negativeSign, Class numberClass, 099 NumberFormat format) { 100 this(nrOfNonDecimals, nrOfDecimals, negativeSign, numberClass, format, DEFAULT_UNFORMAT); 101 } 102 103 /** 104 * @param nrOfNonDecimals Number of non-decimals. 105 * @param nrOfDecimals Number of decimals. 106 * @param negativeSign Negative numbers allowed. 107 * @param numberClass Class type (default BigDecimal). 108 * @param format The "read"-format. 109 * @param unformat The "edit"-format. 110 */ 111 public BigDecimalTextField(int nrOfNonDecimals, int nrOfDecimals, boolean negativeSign, Class numberClass, 112 NumberFormat format, NumberFormat unformat) { 113 super(); 114 Assert.notNull(format); 115 Assert.notNull(unformat); 116 this.format = format; 117 setBigDecimalFormat(format, numberClass); 118 this.unformat = unformat; 119 setBigDecimalFormat(unformat, numberClass); 120 this.numberClass = numberClass; 121 setDocument(new BigDecimalDocument(nrOfNonDecimals, nrOfDecimals, negativeSign)); 122 addFocusListener(new FormatFocusListener()); 123 } 124 125 /** 126 * When parsing a number, BigDecimalFormat can return numbers different than 127 * BigDecimal. This method will ensure that when using a {@link BigDecimal} 128 * or a {@link BigInteger}, the formatter will return a {@link BigDecimal} 129 * in order to prevent loss of precision. Note that you should use the 130 * {@link DecimalFormat} to make this work. 131 * 132 * @param format 133 * @param numberClass 134 * 135 * @see #getValue() 136 * @see DecimalFormat#setParseBigDecimal(boolean) 137 */ 138 private static final void setBigDecimalFormat(NumberFormat format, Class numberClass) { 139 if (format instanceof DecimalFormat && ((numberClass == BigDecimal.class) || (numberClass == BigInteger.class))) { 140 ((DecimalFormat) format).setParseBigDecimal(true); 141 } 142 } 143 144 /** 145 * Add a UserInputListener. 146 * 147 * @param listener UserInputListener. 148 * 149 * @see UserInputListener 150 */ 151 public void addUserInputListener(UserInputListener listener) { 152 if (this.listeners == null) 153 this.listeners = new ArrayList(); 154 this.listeners.add(listener); 155 } 156 157 /** 158 * Remove a UserInputListener. 159 * 160 * @param listener UserInputListener. 161 * 162 * @see UserInputListener 163 */ 164 public void removeUserInputListener(UserInputListener listener) { 165 if (listeners != null) { 166 this.listeners.remove(listener); 167 } 168 } 169 170 /** 171 * Fire an event to all UserInputListeners. 172 */ 173 private void fireUserInputChange() { 174 if (!internallySettingText && (this.listeners != null)) { 175 for (Iterator it = this.listeners.iterator(); it.hasNext();) { 176 UserInputListener userInputListener = (UserInputListener) it.next(); 177 userInputListener.update(this); 178 } 179 } 180 } 181 182 /** 183 * Parses a number from the inputField and will adjust it's class if needed. 184 * 185 * @return Number the Parsed number. 186 */ 187 public Number getValue() { 188 if ((getText() == null) || "".equals(getText().trim())) 189 return null; 190 try { 191 Number n = format.parse(getText()); 192 if (n.getClass() == this.numberClass) 193 return n; 194 else if (this.numberClass == BigDecimal.class) { 195 BigDecimal bd = new BigDecimal(n.doubleValue()); 196 if (scale != null) { 197 bd = bd.setScale(scale.intValue(), BigDecimal.ROUND_HALF_UP); 198 } 199 return bd; 200 } 201 else if (this.numberClass == Double.class) 202 return new Double(n.doubleValue()); 203 else if (this.numberClass == Float.class) 204 return new Float(n.floatValue()); 205 else if (this.numberClass == BigInteger.class) 206 // we have called setBigDecimalFormat to make sure a BigDecimal 207 // is returned so use toBigInteger on that class 208 return ((BigDecimal) n).toBigInteger(); 209 else if (this.numberClass == Long.class) 210 return new Long(n.longValue()); 211 else if (this.numberClass == Integer.class) 212 return new Integer(n.intValue()); 213 else if (this.numberClass == Short.class) 214 return new Short(n.shortValue()); 215 else if (this.numberClass == Byte.class) 216 return new Byte(n.byteValue()); 217 return null; 218 } 219 catch (Exception pe) { 220 log.error("Error: " + getText() + " is not a number.", pe); 221 return null; 222 } 223 } 224 225 /** 226 * Format the number and show it. 227 * 228 * @param number Number to set. 229 */ 230 public void setValue(Number number) { 231 String txt = null; 232 if (number != null) { 233 txt = this.format.format(number); 234 } 235 setText(txt); 236 } 237 238 /** 239 * Set text internally: will change text but not fire any event. 240 * 241 * @param s Text to set. 242 */ 243 private void setTextInternally(String s) { 244 internallySettingText = true; 245 setText(s); 246 internallySettingText = false; 247 } 248 249 /** 250 * <p> 251 * When inputField gets focus, the contents will switch to "edit"-format 252 * (=unformat). In most cases a format without all decorations, just the 253 * number. In addition a selectAll() will be done. 254 * </p> 255 * 256 * TODO check if selectAll() is appropriate in all cases. 257 * 258 * <p> 259 * When inputField loses focus, the contents will switch to "read"-format 260 * (=format). This will probably contain some decorations. 261 * </p> 262 */ 263 class FormatFocusListener implements FocusListener { 264 265 /** 266 * Focus gained: "edit"-format and selectAll. 267 */ 268 public void focusGained(FocusEvent e) { 269 String s = getText(); 270 setTextInternally(format(unformat, format, s)); 271 selectAll(); 272 } 273 274 /** 275 * Focus lost: "read"-format. 276 */ 277 public void focusLost(FocusEvent e) { 278 String s = getText(); 279 setTextInternally(format(format, unformat, s)); 280 } 281 282 /** 283 * Format a string. 284 * 285 * @param toFormat Change to this format. 286 * @param fromFormat Current format to be changed. 287 * @param s String to be reformatted. 288 * @return String which holds the number in the new format. 289 */ 290 private String format(NumberFormat toFormat, NumberFormat fromFormat, String s) { 291 if (!"".equals(s)) { 292 try { 293 return toFormat.format(fromFormat.parse(s)); 294 } 295 catch (ParseException pe) { 296 log.error("Fout: De ingevulde waarde " + getText() + " is geen nummer.", pe); 297 } 298 } 299 return null; 300 } 301 } 302 303 /** 304 * Specific document that allows only input of numbers, decimal separator 305 * (or alternative) and sign. Maximum number of decimals/non-decimals will 306 * be respected at all times. Signing can be changed anywhere in the 307 * inputField by simply clicking +/-. Decimal separator input can be done 308 * with alternative character to allow both comma and point. 309 * 310 * @author jh 311 */ 312 class BigDecimalDocument extends PlainDocument { 313 314 private final int nrOfNonDecimals; 315 316 private final int nrOfDecimals; 317 318 private final boolean negativeSign; 319 320 private final char decimalSeparator = symbols.getDecimalSeparator(); 321 322 private final char alternativeSeparator; 323 324 /** 325 * @see #BigDecimalDocument(int, int, boolean, char) 326 */ 327 public BigDecimalDocument() { 328 this(10, 2, true); 329 } 330 331 /** 332 * @see #BigDecimalDocument(int, int, boolean, char) 333 */ 334 public BigDecimalDocument(int nrOfNonDecimals, int nrOfDecimals, boolean negativeSign) { 335 this(nrOfNonDecimals, nrOfDecimals, negativeSign, symbols.getGroupingSeparator()); 336 } 337 338 /** 339 * Constructor with several configurations. Alternative separator can be 340 * given in order to make input easier. Eg. Comma and point can be used 341 * for decimal separation. 342 * 343 * @param nrOfNonDecimals Maximum number of non-decimals. 344 * @param nrOfDecimals Maximum number of decimals. 345 * @param negativeSign Negative sign allowed. 346 * @param alternativeSeparator Alternative separator. 347 */ 348 public BigDecimalDocument(int nrOfNonDecimals, int nrOfDecimals, boolean negativeSign, char alternativeSeparator) { 349 this.nrOfNonDecimals = nrOfNonDecimals; 350 this.nrOfDecimals = nrOfDecimals; 351 this.negativeSign = negativeSign; 352 this.alternativeSeparator = alternativeSeparator; 353 } 354 355 /** 356 * Handles string insertion, checks several things like number of 357 * non-decimals/decimals/sign... 358 * 359 * @inheritDoc 360 */ 361 public void insertString(int offset, String str, AttributeSet a) throws BadLocationException { 362 // first doing the single keys, then review what can be used for 363 // cut/paste actions 364 if ("-".equals(str)) { 365 if (this.negativeSign) // set - or flip to + if it's already 366 // there 367 { 368 if ((this.getLength() == 0) || !this.getText(0, 1).equals("-")) 369 super.insertString(0, str, a); 370 else if (!(this.getLength() == 0) && this.getText(0, 1).equals("-")) 371 super.remove(0, 1); 372 fireUserInputChange(); 373 } 374 return; 375 } 376 else if ("+".equals(str)) { 377 if (this.negativeSign && (!(this.getLength() == 0) && this.getText(0, 1).equals("-"))) { 378 super.remove(0, 1); 379 fireUserInputChange(); 380 } 381 return; 382 } 383 else if (isShortCut(str)) 384 { 385 handleShortCut(str, offset, a); 386 return; 387 } 388 // check decimal signs 389 else if ((str.length() == 1) 390 && ((this.alternativeSeparator == str.charAt(0)) || (this.decimalSeparator == str.charAt(0)))) { 391 if ((nrOfDecimals > 0) && (nrOfDecimals >= (getLength() - offset)) 392 && (getText(0, getLength()).indexOf(this.decimalSeparator) == -1)) { 393 super.insertString(offset, Character.toString(this.decimalSeparator), a); 394 fireUserInputChange(); 395 } 396 return; 397 } 398 String s = getText(0, offset) + str; 399 if (offset < getLength()) { 400 s += getText(offset, getLength() - offset); 401 } 402 403 boolean isNegative = s.startsWith("-"); 404 char[] sarr = isNegative ? s.substring(1).toCharArray() : s.toCharArray(); 405 int sep = -1; 406 int numberLength = 0; // count numbers, no special characters 407 for (int i = 0; i < sarr.length; i++) { 408 if (sarr[i] == this.decimalSeparator) { 409 if (sep != -1) {// double decimalseparator?? 410 log 411 .warn("Error while inserting string: " + s + "[pos=" + i + "]" 412 + " Double decimalseparator?"); 413 return; 414 } 415 sep = i; 416 if (numberLength > this.nrOfNonDecimals) {// too many 417 // digits left 418 // of decimal 419 // separator 420 log.warn("Error while inserting string: " + s + "[pos=" + i + "]" + " Too many non decimals? [" 421 + this.nrOfNonDecimals + "]"); 422 return; 423 } 424 else if ((sarr.length - sep - 1) > this.nrOfDecimals) {// too 425 // many 426 // digits 427 // right 428 // of 429 // decimal 430 // separator 431 log.warn("Error while inserting string: " + s + "[pos=" + i + "]" + " Too many decimals? [" 432 + this.nrOfDecimals + "]"); 433 return; 434 } 435 } 436 else if (sarr[i] == symbols.getGroupingSeparator()) { 437 // ignore character 438 } 439 else if (!Character.isDigit(sarr[i])) {// non digit, no 440 // grouping/decimal 441 // separator not allowed 442 log.warn("Error while inserting string: " + s + "[pos=" + i + "]" 443 + " String contains character that is no digit or separator?"); 444 return; 445 } 446 else 447 ++numberLength; 448 } 449 if ((sep == -1) && (numberLength > this.nrOfNonDecimals)) {// no 450 // separator, 451 // number 452 // too 453 // big 454 log.warn("Error while inserting string: " + s + " Too many non decimals? [" + this.nrOfNonDecimals 455 + "]"); 456 return; 457 } 458 super.insertString(offset, str, a); 459 fireUserInputChange(); 460 } 461 462 private void handleShortCut(String str, int offset, AttributeSet a) throws BadLocationException 463 { 464 log.debug("handing shortcut " + str); 465 if (getLength() == 0) 466 { 467 if (str.equals("k")) 468 { 469 super.insertString(0, "1000", a); 470 } 471 else if (str.equals("m")) 472 { 473 super.insertString(0, "1000000", a); 474 } 475 else if (str.equals("b")) 476 { 477 super.insertString(0, "1000000000", a); 478 } 479 } 480 else if (getLength() == 1 && (getText(0, 1).equals("-") || getText(0, 1).equals("+"))) 481 { 482 } 483 else 484 { 485 String text = getText(0, offset); 486 text = text.replace(',', '.'); 487 BigDecimal dec = new BigDecimal(text); 488 if (str.equals("k")) 489 { 490 dec = dec.scaleByPowerOfTen(3); 491 } 492 else if (str.equals("m")) 493 { 494 dec = dec.scaleByPowerOfTen(6); 495 } 496 else if (str.equals("b")) 497 { 498 dec = dec.scaleByPowerOfTen(9); 499 } 500 super.remove(0, offset); 501 String outcome = dec.toBigIntegerExact().toString(); 502 outcome = outcome.replace('.', decimalSeparator); 503 super.insertString(0, outcome, a); 504 fireUserInputChange(); 505 } 506 507 } 508 509 private boolean isShortCut(String str) 510 { 511 return str.equals("k") || str.equals("m") || str.equals("b"); 512 } 513 514 /** 515 * Will trigger the UserInputListeners once after removing. 516 * 517 * @inheritDoc 518 */ 519 public void remove(int offs, int len) throws BadLocationException { 520 super.remove(offs, len); 521 fireUserInputChange(); 522 } 523 524 /** 525 * Will trigger the UserInputListeners once after replacing. 526 * 527 * @inheritDoc 528 */ 529 public void replace(int offset, int length, String text, AttributeSet attrs) throws BadLocationException { 530 boolean oldInternallySettingText = internallySettingText; 531 internallySettingText = true; 532 super.replace(offset, length, text, attrs); 533 internallySettingText = oldInternallySettingText; 534 fireUserInputChange(); 535 } 536 } 537 538 /** 539 * @return Returns the scale. 540 */ 541 public Integer getScale() { 542 return scale; 543 } 544 545 /** 546 * @param scale The scale to set. 547 */ 548 public void setScale(Integer scale) { 549 this.scale = scale; 550 } 551 }