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 }