1 package fr.ifremer.quadrige3.ui.swing.component.number;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 import com.google.common.base.Preconditions;
25 import com.google.common.collect.ImmutableSet;
26 import com.google.common.collect.Sets;
27 import jaxx.runtime.spi.UIHandler;
28 import org.apache.commons.lang3.StringUtils;
29 import org.apache.commons.logging.Log;
30 import org.apache.commons.logging.LogFactory;
31 import org.nuiton.jaxx.widgets.MutateOnConditionalPropertyChangeListener;
32 import org.nuiton.util.beans.BeanUtil;
33
34 import javax.swing.JComponent;
35 import javax.swing.JTextField;
36 import javax.swing.SwingUtilities;
37 import javax.swing.text.AbstractDocument;
38 import javax.swing.text.AttributeSet;
39 import javax.swing.text.BadLocationException;
40 import javax.swing.text.DocumentFilter;
41 import java.awt.Dimension;
42 import java.awt.event.MouseAdapter;
43 import java.awt.event.MouseEvent;
44 import java.lang.reflect.Method;
45 import java.math.BigDecimal;
46 import java.math.BigInteger;
47 import java.text.DecimalFormat;
48 import java.util.HashMap;
49 import java.util.Map;
50 import java.util.regex.Matcher;
51 import java.util.regex.Pattern;
52
53
54
55
56
57
58
59
60
61
62 public class NumberEditorHandler implements UIHandler<NumberEditor> {
63
64
65 private static final Log log = LogFactory.getLog(NumberEditorHandler.class);
66
67 protected final static ImmutableSet<Class<?>> INT_CLASSES = ImmutableSet.copyOf(Sets.<Class<?>>newHashSet(
68 byte.class,
69 Byte.class,
70 short.class,
71 Short.class,
72 int.class,
73 Integer.class,
74 BigInteger.class
75 ));
76
77 private static final String VALIDATE_PROPERTY = "validate";
78
79 protected NumberEditor ui;
80
81 protected Pattern numberPattern;
82
83 protected NumberParserFormatter<Number> numberParserFormatter;
84
85 private static Map<Class<?>, NumberParserFormatter<?>> numberFactories;
86
87 @Override
88 public void beforeInit(NumberEditor ui) {
89 this.ui = ui;
90
91 NumberEditorModel model = new NumberEditorModel(new NumberEditorConfig());
92 ui.setContextValue(model);
93 }
94
95 @Override
96 public void afterInit(NumberEditor ui) {
97
98 }
99
100
101
102
103
104
105
106 public void addChar(char c) {
107 try {
108
109 ui.getTextField().getDocument().insertString(ui.getTextField().getCaretPosition(), c + "", null);
110
111 } catch (BadLocationException e) {
112 log.warn(e);
113 }
114
115 setTextValue(ui.getTextField().getText());
116
117 }
118
119
120
121
122
123 public void removeChar() {
124
125 JTextField textField = ui.getTextField();
126
127 int position = textField.getCaretPosition();
128 if (position < 1) {
129 if (log.isDebugEnabled()) {
130 log.debug("cannot remove when caret on first position or text empty");
131 }
132
133 return;
134 }
135 try {
136 textField.getDocument().remove(position - 1, 1);
137 } catch (BadLocationException ex) {
138
139 log.debug(ex);
140 return;
141 }
142 String newText = textField.getText();
143 if (log.isDebugEnabled()) {
144 log.debug("text updated : " + newText);
145 }
146 position--;
147 textField.setCaretPosition(position);
148
149 setTextValue(newText);
150
151 }
152
153 public void reset() {
154
155
156
157 setTextValue("");
158
159 }
160
161
162
163
164
165 public void toggleSign() {
166
167 String newValue = ui.getModel().getTextValue();
168
169 if (newValue.startsWith("-")) {
170 newValue = newValue.substring(1);
171 } else {
172 newValue = "-" + newValue;
173 }
174
175 setTextValue(newValue);
176
177 }
178
179 public void setTextValue(String newText) {
180
181 NumberEditorModel model = ui.getModel();
182
183 boolean textValid;
184
185 if (StringUtils.isEmpty(newText)) {
186
187
188 textValid = true;
189
190 } else {
191
192 if (numberPattern != null) {
193
194
195 Matcher matcher = numberPattern.matcher(newText);
196
197 textValid = matcher.matches();
198
199 if (!textValid && model.isCanUseSign() && "-".equals(newText)) {
200
201 textValid = true;
202 }
203
204 } else {
205
206
207
208 textValid = true;
209
210 }
211
212 }
213
214 if (textValid) {
215
216 if (log.isDebugEnabled()) {
217 log.debug("Text [" + newText + "] is valid, will set it to model");
218 }
219
220 model.setTextValue(newText);
221
222 } else {
223
224 String oldText = model.getTextValue();
225
226 if (oldText == null) {
227
228 oldText = "";
229
230 }
231
232 if (log.isDebugEnabled()) {
233 log.debug("Text [" + newText + "] is not valid, will rollback to previous valid text: " + oldText);
234 }
235
236 int caretPosition = ui.getTextField().getCaretPosition() - 1;
237
238 ui.getTextField().setText(oldText);
239 if (caretPosition >= 0) {
240 ui.getTextField().setCaretPosition(caretPosition);
241 }
242
243 }
244
245 }
246
247
248
249
250
251
252 public void setPopupVisible(Boolean newValue) {
253
254 if (log.isTraceEnabled()) {
255 log.trace(newValue);
256 }
257
258 if (newValue == null || !newValue) {
259 ui.getPopup().setVisible(false);
260 return;
261 }
262 SwingUtilities.invokeLater(() -> {
263 JComponent invoker =
264 ui.isShowPopupButton() ?
265 ui.getShowPopUpButton() :
266 ui;
267 Dimension dim = ui.getPopup().getPreferredSize();
268 int x = (int) (invoker.getPreferredSize().getWidth() - dim.getWidth());
269 ui.getPopup().show(invoker, x, invoker.getHeight());
270 ui.getTextField().requestFocus();
271 });
272 }
273
274 protected void init() {
275
276 NumberEditorModel model = ui.getModel();
277
278 NumberEditorConfig config = model.getConfig();
279
280 Class<?> numberType = config.getNumberType();
281
282 {
283
284 Preconditions.checkState(numberType != null, "Required a number type on " + ui);
285
286 numberParserFormatter = getNumberFactory(numberType);
287
288 if (log.isDebugEnabled()) {
289 log.debug("init numberType: " + numberType + " on " + ui);
290 }
291
292 }
293
294 {
295
296
297 Boolean useDecimal = config.getUseDecimal();
298
299 boolean canBeDecimal = !INT_CLASSES.contains(numberType);
300
301 if (useDecimal == null) {
302
303
304 useDecimal = canBeDecimal;
305
306 config.setUseDecimal(useDecimal);
307
308 } else {
309
310
311 Preconditions.checkState(!useDecimal || canBeDecimal, "Can't use decimal with the following number type " + numberType + " on " + ui);
312
313 }
314
315 }
316
317 {
318
319
320 String numberPatternDef = model.getNumberPattern();
321 if (StringUtils.isNotEmpty(numberPatternDef)) {
322
323 setNumberPattern(numberPatternDef);
324
325 }
326 }
327
328
329 {
330
331
332 model.addPropertyChangeListener(NumberEditorModel.PROPERTY_NUMBER_PATTERN, evt -> {
333 String newPattern = (String) evt.getNewValue();
334
335 setNumberPattern(newPattern);
336
337 if (log.isDebugEnabled()) {
338 log.debug("set new numberPattern" + newPattern);
339 }
340 if (StringUtils.isEmpty(newPattern)) {
341 numberPattern = null;
342 } else {
343 numberPattern = Pattern.compile(newPattern);
344 }
345 });
346
347
348 model.addPropertyChangeListener(NumberEditorModel.PROPERTY_NUMBER_VALUE, evt -> {
349
350 Number newValue = (Number) evt.getNewValue();
351 setTextValueFromNumberValue(newValue);
352
353
354 });
355
356
357 model.addPropertyChangeListener(NumberEditorModel.PROPERTY_TEXT_VALUE, evt -> {
358
359 String newValue = (String) evt.getNewValue();
360 setNumberValueFromTextValue(newValue);
361
362 });
363
364 }
365 Object bean = model.getBean();
366
367 if (bean != null) {
368
369 String property = config.getProperty();
370
371 if (property != null) {
372
373 Method mutator = BeanUtil.getMutator(bean, property);
374
375
376 Preconditions.checkNotNull(mutator, "could not find mutator for " + property);
377
378
379 Class<?>[] parameterTypes = mutator.getParameterTypes();
380 Preconditions.checkArgument(parameterTypes[0].equals(numberType), "Mismatch mutator type, required a " + numberType + " but was " + parameterTypes[0]);
381
382
383 model.addPropertyChangeListener(
384 NumberEditorModel.PROPERTY_NUMBER_VALUE,
385 new MutateOnConditionalPropertyChangeListener<>(model, mutator, model.canUpdateBeanNumberValuePredicate()));
386
387 }
388 }
389
390 {
391
392
393 ui.addPropertyChangeListener(NumberEditor.PROPERTY_SHOW_POPUP_BUTTON, evt -> {
394 if (ui.getPopup().isVisible()) {
395 setPopupVisible(false);
396 }
397 });
398
399 ui.addPropertyChangeListener(NumberEditor.PROPERTY_AUTO_POPUP, evt -> {
400 if (ui.getPopup().isVisible()) {
401 setPopupVisible(false);
402 }
403 });
404
405 ui.addPropertyChangeListener(NumberEditor.PROPERTY_POPUP_VISIBLE, evt -> setPopupVisible((Boolean) evt.getNewValue()));
406 ui.getTextField().addMouseListener(new PopupListener());
407
408 }
409
410
411 Number numberValue = model.getNumberValue();
412 setTextValueFromNumberValue(numberValue);
413
414
415 ((AbstractDocument) ui.getTextField().getDocument()).setDocumentFilter(new CommaFilter());
416
417 }
418
419 protected void setNumberPattern(String newPattern) {
420
421 try {
422 this.numberPattern = Pattern.compile(newPattern);
423 } catch (Exception e) {
424 throw new IllegalStateException("Could not compute numberPattern " + newPattern + " on " + ui, e);
425 }
426
427 if (log.isDebugEnabled()) {
428 log.debug("init numberPattern: " + numberPattern + " on " + ui);
429 }
430
431 }
432
433 protected void setNumberValueFromTextValue(String textValue) {
434
435 if (ui.getModel().isNumberValueIsAdjusting()) {
436
437 return;
438 }
439
440 Number numberValue;
441
442 if (StringUtils.isBlank(textValue)) {
443
444 numberValue = null;
445
446 } else {
447
448 numberValue = numberParserFormatter.parse(textValue);
449
450 }
451
452 if (log.isDebugEnabled()) {
453 log.debug("Set numberValue " + numberValue + " from textValue " + textValue);
454 }
455 ui.getModel().setNumberValue(numberValue);
456 }
457
458 protected void setTextValueFromNumberValue(Number numberValue) {
459
460 if (ui.getModel().isTextValueIsAdjusting()) {
461
462 return;
463 }
464
465 String textValue = numberParserFormatter.format(numberValue);
466
467 if (log.isDebugEnabled()) {
468 log.debug("Set textValue " + textValue + " from numberValue " + numberValue);
469 }
470 ui.getModel().setTextValue(textValue);
471
472 }
473
474 protected void validate() {
475
476 setPopupVisible(false);
477
478 ui.firePropertyChange(VALIDATE_PROPERTY, null, true);
479 }
480
481 protected class PopupListener extends MouseAdapter {
482
483 @Override
484 public void mousePressed(MouseEvent e) {
485 maybeShowPopup(e);
486 }
487
488 @Override
489 public void mouseReleased(MouseEvent e) {
490 maybeShowPopup(e);
491 }
492
493 protected void maybeShowPopup(MouseEvent e) {
494 if (!e.isPopupTrigger()) {
495 return;
496 }
497 if (ui.isAutoPopup()) {
498 if (ui.isPopupVisible()) {
499 if (!ui.getPopup().isVisible()) {
500 setPopupVisible(true);
501 }
502
503
504 } else {
505
506 ui.setPopupVisible(true);
507
508 }
509 } else {
510 if (ui.isPopupVisible()) {
511 setPopupVisible(true);
512 }
513 }
514 }
515 }
516
517 interface NumberParserFormatter<N extends Number> {
518
519 N parse(String textValue);
520
521 String format(N numberValue);
522
523 }
524
525
526 protected static NumberParserFormatter<Number> getNumberFactory(final Class<?> numberType) {
527
528 if (numberFactories == null) {
529
530 numberFactories = new HashMap<>();
531
532 NumberParserFormatter<Byte> byteSupport = new NumberParserFormatter<Byte>() {
533 @Override
534 public String format(Byte numberValue) {
535 return numberValue == null ? "" : String.valueOf(numberValue);
536 }
537
538 @Override
539 public Byte parse(String textValue) {
540 Byte v;
541 if (NULL_LIMIT_INTS.contains(textValue)) {
542 v = null;
543 } else {
544 v = Byte.parseByte(textValue);
545 }
546 return v;
547 }
548 };
549 numberFactories.put(byte.class, byteSupport);
550 numberFactories.put(Byte.class, byteSupport);
551
552 NumberParserFormatter<Short> shortSupport = new NumberParserFormatter<Short>() {
553 @Override
554 public String format(Short numberValue) {
555 return numberValue == null ? "" : String.valueOf(numberValue);
556 }
557
558 @Override
559 public Short parse(String textValue) {
560 Short v;
561 if (NULL_LIMIT_INTS.contains(textValue)) {
562 v = null;
563 } else {
564 v = Short.parseShort(textValue);
565 }
566 return v;
567 }
568 };
569 numberFactories.put(short.class, shortSupport);
570 numberFactories.put(Short.class, shortSupport);
571
572 NumberParserFormatter<Integer> integerSupport = new NumberParserFormatter<Integer>() {
573 @Override
574 public String format(Integer numberValue) {
575 return numberValue == null ? "" : String.valueOf(numberValue);
576 }
577
578 @Override
579 public Integer parse(String textValue) {
580 Integer v;
581 if (NULL_LIMIT_INTS.contains(textValue)) {
582 v = null;
583 } else {
584 v = Integer.parseInt(textValue);
585 }
586 return v;
587 }
588 };
589 numberFactories.put(int.class, integerSupport);
590 numberFactories.put(Integer.class, integerSupport);
591
592 NumberParserFormatter<Long> longSupport = new NumberParserFormatter<Long>() {
593 @Override
594 public String format(Long numberValue) {
595 return numberValue == null ? "" : String.valueOf(numberValue);
596 }
597
598 @Override
599 public Long parse(String textValue) {
600 Long v;
601 if (NULL_LIMIT_INTS.contains(textValue)) {
602 v = null;
603 } else {
604 v = Long.parseLong(textValue);
605 }
606 return v;
607 }
608 };
609 numberFactories.put(long.class, longSupport);
610 numberFactories.put(Long.class, longSupport);
611
612 NumberParserFormatter<Float> floatSupport = new NumberParserFormatter<Float>() {
613
614 final DecimalFormat df = new DecimalFormat("#.##########");
615
616 @Override
617 public String format(Float numberValue) {
618 String format;
619 if (numberValue == null) {
620 format = "";
621 } else {
622 format = String.valueOf(numberValue);
623 if (format.contains("E")) {
624
625 format = df.format(numberValue);
626 if (format.contains(",")) {
627 format = format.replace(",", ".");
628 }
629
630 }
631 }
632 return format;
633 }
634
635 @Override
636 public Float parse(String textValue) {
637 Float v;
638 if (NULL_LIMIT_DECIMALS.contains(textValue)) {
639 v = null;
640 } else {
641 boolean addSign = false;
642 if (textValue.startsWith("-")) {
643 addSign = true;
644 textValue = textValue.substring(1);
645 if (textValue.startsWith(".")) {
646 textValue = "0" + textValue;
647 }
648 }
649 v = Float.parseFloat(textValue);
650 if (addSign) {
651 v = -v;
652 }
653 }
654 return v;
655 }
656 };
657 numberFactories.put(float.class, floatSupport);
658 numberFactories.put(Float.class, floatSupport);
659
660 NumberParserFormatter<Double> doubleSupport = new NumberParserFormatter<Double>() {
661
662 final DecimalFormat df = new DecimalFormat("#.##########");
663
664 @Override
665 public String format(Double numberValue) {
666 String format;
667 if (numberValue == null) {
668 format = "";
669 } else {
670 format = String.valueOf(numberValue);
671 if (format.contains("E")) {
672
673 format = df.format(numberValue);
674 if (format.contains(",")) {
675 format = format.replace(",", ".");
676 }
677
678 }
679 }
680 return format;
681 }
682
683 @Override
684 public Double parse(String textValue) {
685 Double v;
686 if (NULL_LIMIT_DECIMALS.contains(textValue)) {
687 v = null;
688 } else {
689
690 boolean addSign = false;
691 if (textValue.startsWith("-")) {
692 addSign = true;
693 textValue = textValue.substring(1);
694 if (textValue.startsWith(".")) {
695 textValue = "0" + textValue;
696 }
697 }
698 v = Double.parseDouble(textValue);
699 if (addSign) {
700 v = -v;
701 }
702
703 }
704 return v;
705 }
706 };
707 numberFactories.put(double.class, doubleSupport);
708 numberFactories.put(Double.class, doubleSupport);
709 NumberParserFormatter<BigInteger> bigIntegerSupport = new NumberParserFormatter<BigInteger>() {
710 @Override
711 public String format(BigInteger numberValue) {
712 return numberValue == null ? "" : String.valueOf(numberValue);
713 }
714
715 @Override
716 public BigInteger parse(String textValue) {
717
718 if (NULL_LIMIT_INTS.contains(textValue))
719 return null;
720
721 return new BigInteger(textValue);
722 }
723 };
724 numberFactories.put(BigInteger.class, bigIntegerSupport);
725 NumberParserFormatter<BigDecimal> bigDecimalSupport = new NumberParserFormatter<BigDecimal>() {
726 @Override
727 public String format(BigDecimal numberValue) {
728 return numberValue == null ? "" : String.valueOf(numberValue);
729 }
730
731 @Override
732 public BigDecimal parse(String textValue) {
733
734
735 if (NULL_LIMIT_DECIMALS.contains(textValue))
736 return null;
737
738 return new BigDecimal(textValue);
739 }
740 };
741 numberFactories.put(BigDecimal.class, bigDecimalSupport);
742
743 }
744
745 for (Map.Entry<Class<?>, NumberParserFormatter<?>> entry : numberFactories.entrySet()) {
746
747 if (entry.getKey().equals(numberType)) {
748
749
750 return (NumberParserFormatter<Number>) entry.getValue();
751
752 }
753
754 }
755
756 throw new IllegalArgumentException("Could not find a NumberFactory for type " + numberType);
757
758 }
759
760 static class CommaFilter extends DocumentFilter {
761
762 @Override
763 public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
764 super.replace(fb, offset, length, filter(text), attrs);
765 }
766
767 @Override
768 public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException {
769 super.insertString(fb, offset, filter(string), attr);
770 }
771
772 private String filter(String input) {
773 if (StringUtils.isBlank(input))
774 return input;
775
776
777 return input.replaceAll(",", ".");
778 }
779 }
780
781 protected static final ImmutableSet<String> NULL_LIMIT_DECIMALS = ImmutableSet.copyOf(new String[]{"-", ".", "-."});
782
783 protected static final ImmutableSet<String> NULL_LIMIT_INTS = ImmutableSet.copyOf(new String[]{"-"});
784
785 }