View Javadoc
1   package fr.ifremer.quadrige3.ui.swing.component.number;
2   
3   /*
4    * #%L
5    * JAXX :: Widgets
6    * %%
7    * Copyright (C) 2008 - 2014 CodeLutin
8    * %%
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Affero General Public License as published by
11   * the Free Software Foundation, either version 3 of the License, or
12   * (at your option) any later version.
13   * 
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Public License for more details.
18   * 
19   * You should have received a copy of the GNU Affero General Public License
20   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21   * #L%
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   * Created on 11/23/14.
55   * Refactored on 07/02/2020 - Add filter on comma character (Mantis #50209)
56   * Refactored on 28/07/2020 - Add safe leading dot parser for BigDecimal (Mantis #52676)
57   *
58   * @author Tony Chemit - chemit@codelutin.com
59   * @author Ludovic Pecquot (ludovic.pecquot@e-is.pro)
60   * @since 2.17
61   */
62  public class NumberEditorHandler implements UIHandler<NumberEditor> {
63  
64      /** Logger. */
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          // nothing to do here, everything is done in init method
98      }
99  
100     /**
101      * Ajoute le caractère donné à l'endroit où est le curseur dans la zone de
102      * saisie et met à jour le modèle.
103      *
104      * @param c le caractère à ajouter.
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      * Supprime le caractère juste avant le curseur du modèle (textuel) et
121      * met à jour la zone de saisie.
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             // on est au debut du doc, donc rien a faire
133             return;
134         }
135         try {
136             textField.getDocument().remove(position - 1, 1);
137         } catch (BadLocationException ex) {
138             // ne devrait jamais arrive vu qu'on a fait le controle...
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 //        ui.getModel().setNumberValue(null);
156 
157         setTextValue("");
158 
159     }
160 
161     /**
162      * Permute le signe dans la zone de saisie et
163      * dans le modèle.
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             // empty text is always valid
188             textValid = true;
189 
190         } else {
191 
192             if (numberPattern != null) {
193 
194                 // use given number pattern to check text
195                 Matcher matcher = numberPattern.matcher(newText);
196 
197                 textValid = matcher.matches();
198 
199                 if (!textValid && model.isCanUseSign() && "-".equals(newText)) {
200                     // limit case when we have only "-"
201                     textValid = true;
202                 }
203 
204             } else {
205 
206                 // check text validity "by hand"
207                 //TODO
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      * Affiche ou cache la popup.
249      *
250      * @param newValue la nouvelle valeur de visibilité de la popup.
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             // init numberType
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             // init canUseDecimal
297             Boolean useDecimal = config.getUseDecimal();
298 
299             boolean canBeDecimal = !INT_CLASSES.contains(numberType);
300 
301             if (useDecimal == null) {
302 
303                 // guess from type
304                 useDecimal = canBeDecimal;
305 
306                 config.setUseDecimal(useDecimal);
307 
308             } else {
309 
310                 // Check this is possible
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             // init numberPattern
320             String numberPatternDef = model.getNumberPattern();
321             if (StringUtils.isNotEmpty(numberPatternDef)) {
322 
323                 setNumberPattern(numberPatternDef);
324 
325             }
326         }
327 
328 
329         {
330 
331             // list when number pattern changed to recompute it
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             // listen when numberValue changed (should be from outside) to convert to textValue
348             model.addPropertyChangeListener(NumberEditorModel.PROPERTY_NUMBER_VALUE, evt -> {
349 
350                 Number newValue = (Number) evt.getNewValue();
351                 setTextValueFromNumberValue(newValue);
352 
353 
354             });
355 
356             // listen when textValue changed to convert to number value
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                 // check mutator exists
376                 Preconditions.checkNotNull(mutator, "could not find mutator for " + property);
377 
378                 // check type is ok
379                 Class<?>[] parameterTypes = mutator.getParameterTypes();
380                 Preconditions.checkArgument(parameterTypes[0].equals(numberType), "Mismatch mutator type, required a " + numberType + " but was " + parameterTypes[0]);
381 
382                 // When model number changed, let's push it back in bean
383                 model.addPropertyChangeListener(
384                         NumberEditorModel.PROPERTY_NUMBER_VALUE,
385                     new MutateOnConditionalPropertyChangeListener<>(model, mutator, model.canUpdateBeanNumberValuePredicate()));
386 
387             }
388         }
389 
390         {
391             // Add some listeners on ui
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         // apply incoming number value
411         Number numberValue = model.getNumberValue();
412         setTextValueFromNumberValue(numberValue);
413 
414         // Add filter
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             // do nothing if number value is already adjusting
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             // do nothing if text value is already adjusting
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         // fire validate property (to be able to notify listeners)
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                     // popup already visible
503 
504                 } else {
505                     // set popup auto
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                     // Don't try to parse if '.' (Mantis #52676)
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                 //noinspection unchecked
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             // replace comma by dot
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 }