All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nuiton.jaxx.widgets.number.NumberEditorHandler Maven / Gradle / Ivy

There is a newer version: 3.0.0-RC-12
Show newest version
package org.nuiton.jaxx.widgets.number;

/*
 * #%L
 * JAXX :: Widgets Number
 * %%
 * Copyright (C) 2008 - 2020 Code Lutin, Ultreia.io
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 *
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * .
 * #L%
 */

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import io.ultreia.java4all.bean.JavaBean;
import io.ultreia.java4all.jaxx.widgets.BeanUIHandlerSupport;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.nuiton.jaxx.runtime.spi.UIHandler;

import javax.swing.JComponent;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import java.awt.Dimension;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * Created on 11/23/14.
 *
 * @author Tony Chemit - [email protected]
 * @since 2.17
 */
public class NumberEditorHandler extends BeanUIHandlerSupport implements UIHandler {

    protected final static ImmutableSet> INT_CLASSES = ImmutableSet.of(
            byte.class,
            Byte.class,
            short.class,
            Short.class,
            int.class,
            Integer.class,
            BigInteger.class
    );
    protected static final ImmutableSet NULL_LIMIT_DECIMAL = ImmutableSet.copyOf(new String[]{"-", ".", "-."});
    protected static final ImmutableSet NULL_LIMIT_INTEGER = ImmutableSet.copyOf(new String[]{"-"});
    private static final Logger log = LogManager.getLogger(NumberEditorHandler.class);
    private static final String VALIDATE_PROPERTY = "validate";
    private static Map, NumberParserFormatter> numberFactories;
    protected Pattern numberPattern;
    protected NumberParserFormatter numberParserFormatter;

    protected static NumberParserFormatter getNumberFactory(final Class numberType) {

        if (numberFactories == null) {

            numberFactories = new HashMap<>();

            NumberParserFormatter byteSupport = new NumberParserFormatter() {
                @Override
                public String format(Number numberValue) {
                    return numberValue == null ? "" : String.valueOf(numberValue);
                }

                @Override
                public Byte parse(String textValue) {
                    Byte v;
                    if (NULL_LIMIT_INTEGER.contains(textValue)) {
                        v = null;
                    } else {
                        v = Byte.parseByte(textValue);
                    }
                    return v;
                }
            };
            numberFactories.put(byte.class, byteSupport);
            numberFactories.put(Byte.class, byteSupport);

            NumberParserFormatter shortSupport = new NumberParserFormatter() {
                @Override
                public String format(Number numberValue) {
                    return numberValue == null ? "" : String.valueOf(numberValue);
                }

                @Override
                public Short parse(String textValue) {
                    Short v;
                    if (NULL_LIMIT_INTEGER.contains(textValue)) {
                        v = null;
                    } else {
                        v = Short.parseShort(textValue);
                    }
                    return v;
                }
            };
            numberFactories.put(short.class, shortSupport);
            numberFactories.put(Short.class, shortSupport);

            NumberParserFormatter integerSupport = new NumberParserFormatter() {
                @Override
                public String format(Number numberValue) {
                    return numberValue == null ? "" : String.valueOf(numberValue);
                }

                @Override
                public Integer parse(String textValue) {
                    Integer v;
                    if (NULL_LIMIT_INTEGER.contains(textValue)) {
                        v = null;
                    } else {
                        v = Integer.parseInt(textValue);
                    }
                    return v;
                }
            };
            numberFactories.put(int.class, integerSupport);
            numberFactories.put(Integer.class, integerSupport);

            NumberParserFormatter longSupport = new NumberParserFormatter() {
                @Override
                public String format(Number numberValue) {
                    return numberValue == null ? "" : String.valueOf(numberValue);
                }

                @Override
                public Long parse(String textValue) {
                    Long v;
                    if (NULL_LIMIT_INTEGER.contains(textValue)) {
                        v = null;
                    } else {
                        v = Long.parseLong(textValue);
                    }
                    return v;
                }
            };
            numberFactories.put(long.class, longSupport);
            numberFactories.put(Long.class, longSupport);

            NumberParserFormatter floatSupport = new NumberParserFormatter() {

                final DecimalFormat df = new DecimalFormat("#.##########");

                @Override
                public String format(Number numberValue) {
                    String format;
                    if (numberValue == null) {
                        format = "";
                    } else {
                        format = String.valueOf(numberValue);
                        if (format.contains("E")) {

                            format = df.format(numberValue);
                            if (format.contains(",")) {
                                format = format.replace(",", ".");
                            }

                        }
                    }
                    return format;
                }

                @Override
                public Float parse(String textValue) {
                    Float v;
                    if (NULL_LIMIT_DECIMAL.contains(textValue)) {
                        v = null;
                    } else {
                        boolean addSign = false;
                        if (textValue.startsWith("-")) {
                            addSign = true;
                            textValue = textValue.substring(1);
                            if (textValue.startsWith(".")) {
                                textValue = "0" + textValue;
                            }
                        }
                        v = Float.parseFloat(textValue);
                        if (addSign) {
                            v = -v;
                        }
                    }
                    return v;
                }
            };
            numberFactories.put(float.class, floatSupport);
            numberFactories.put(Float.class, floatSupport);

            NumberParserFormatter doubleSupport = new NumberParserFormatter() {

                final DecimalFormat df = new DecimalFormat("#.##########");

                @Override
                public String format(Number numberValue) {
                    String format;
                    if (numberValue == null) {
                        format = "";
                    } else {
                        format = String.valueOf(numberValue);
                        if (format.contains("E")) {

                            format = df.format(numberValue);
                            if (format.contains(",")) {
                                format = format.replace(",", ".");
                            }

                        }
                    }
                    return format;
                }

                @Override
                public Double parse(String textValue) {
                    Double v;
                    if (NULL_LIMIT_DECIMAL.contains(textValue)) {
                        v = null;
                    } else {

                        boolean addSign = false;
                        if (textValue.startsWith("-")) {
                            addSign = true;
                            textValue = textValue.substring(1);
                            if (textValue.startsWith(".")) {
                                textValue = "0" + textValue;
                            }
                        }
                        v = Double.parseDouble(textValue);
                        if (addSign) {
                            v = -v;
                        }

                    }
                    return v;
                }
            };
            numberFactories.put(double.class, doubleSupport);
            numberFactories.put(Double.class, doubleSupport);
            NumberParserFormatter bigIntegerSupport = new NumberParserFormatter() {
                @Override
                public String format(Number numberValue) {
                    return numberValue == null ? "" : String.valueOf(numberValue);
                }

                @Override
                public BigInteger parse(String textValue) {
                    return new BigInteger(textValue);
                }
            };
            numberFactories.put(BigInteger.class, bigIntegerSupport);
            NumberParserFormatter bigDecimalSupport = new NumberParserFormatter() {
                @Override
                public String format(Number numberValue) {
                    return numberValue == null ? "" : String.valueOf(numberValue);
                }

                @Override
                public BigDecimal parse(String textValue) {
                    return new BigDecimal(textValue);
                }
            };
            numberFactories.put(BigDecimal.class, bigDecimalSupport);

        }

        for (Map.Entry, NumberParserFormatter> entry : numberFactories.entrySet()) {

            if (entry.getKey().equals(numberType)) {

                return entry.getValue();

            }

        }

        throw new IllegalArgumentException("Could not find a NumberFactory for type " + numberType);

    }

    @Override
    public void beforeInit(NumberEditor ui) {
        super.beforeInit(ui);
        NumberEditorModel model = new NumberEditorModel(new NumberEditorConfig());
        ui.setContextValue(model);
    }

    @Override
    public void afterInit(NumberEditor ui) {
        // nothing to do here, everything is done in init method
    }

    @Override
    public void init(NumberEditor ui) {
        super.init(ui);
        NumberEditorModel model = ui.getModel();

        {
            // Add some listeners on ui

            ui.addPropertyChangeListener(NumberEditor.PROPERTY_SHOW_POPUP_BUTTON, evt -> {
                if (ui.getPopup().isVisible()) {
                    setPopupVisible(false);
                }
            });

            ui.addPropertyChangeListener(NumberEditor.PROPERTY_AUTO_POPUP, evt -> {
                if (ui.getPopup().isVisible()) {
                    setPopupVisible(false);
                }
            });

            ui.addPropertyChangeListener(NumberEditor.PROPERTY_POPUP_VISIBLE, evt -> setPopupVisible((Boolean) evt.getNewValue()));
            ui.getTextField().addMouseListener(new PopupListener());

        }

        // apply incoming number value
        Number numberValue = model.getNumberValue();
        setTextValueFromNumberValue(numberValue);
    }


    @Override
    protected String getProperty(NumberEditor ui) {
        return ui.getModel().getConfig().getProperty();
    }

    @Override
    protected void prepareInit(String property) {
        log.debug(String.format("init NumberEditor %s", ui.getName()));
        if (property == null) {
            ui.setProperty(ui.getName());
        }
        NumberEditorModel model = ui.getModel();
        NumberEditorConfig config = model.getConfig();

        Class numberType = config.getNumberType();
        {
            // init numberType
            Preconditions.checkState(numberType != null, "Required a number type on " + ui);
            numberParserFormatter = getNumberFactory(numberType);
            log.info("init numberType: " + numberType + " on " + ui);
        }
        {
            // init canUseDecimal
            Boolean useDecimal = config.getUseDecimal();
            boolean canBeDecimal = !INT_CLASSES.contains(numberType);
            if (useDecimal == null) {
                // guess from type
                useDecimal = canBeDecimal;
                config.setUseDecimal(useDecimal);
            } else {
                // Check this is possible
                Preconditions.checkState(!useDecimal || canBeDecimal, "Can't use decimal with the following number type " + numberType + " on " + ui);
            }
        }
        {
            // init numberPattern
            String numberPatternDef = model.getNumberPattern();
            if (StringUtils.isNotEmpty(numberPatternDef)) {
                setNumberPattern(numberPatternDef);
            }
        }
        {
            // list when number pattern changed to recompute it
            model.addPropertyChangeListener(NumberEditorModel.PROPERTY_NUMBER_PATTERN, evt -> {
                String newPattern = (String) evt.getNewValue();
                setNumberPattern(newPattern);
                log.info(String.format("set new numberPattern%s", newPattern));
                if (StringUtils.isEmpty(newPattern)) {
                    numberPattern = null;
                } else {
                    numberPattern = Pattern.compile(newPattern);
                }
            });

            // listen when numberValue changed (should be from outside) to convert to textValue
            model.addPropertyChangeListener(NumberEditorModel.PROPERTY_NUMBER_VALUE, evt -> {
                Number newValue = (Number) evt.getNewValue();
                setTextValueFromNumberValue(newValue);
            });

            // listen when textValue changed to convert to number value
            model.addPropertyChangeListener(NumberEditorModel.PROPERTY_TEXT_VALUE, evt -> {
                String newValue = (String) evt.getNewValue();
                setNumberValueFromTextValue(newValue);
            });
        }
    }

    @Override
    protected void prepareBindFromBean(String property, JavaBean bean) {
        NumberEditorModel model = ui.getModel();
        bean.addPropertyChangeListener(property, e -> {
            Number oldValue = model.getNumberValue();
            Number newValue = (Number) e.getNewValue();
            if (!Objects.equals(oldValue, newValue)) {
                ui.setNumberValue(newValue);
            }
        });
    }

    @Override
    protected void prepareBindToBean(String property, JavaBean bean) {
        NumberEditorModel model = ui.getModel();
        model.addPropertyChangeListener(
                NumberEditorModel.PROPERTY_NUMBER_VALUE, e -> {
                    if (!model.canUpdateBeanNumberValuePredicate().test(model)) {
                        return;
                    }
                    bean.set(property, e.getNewValue());
                });
    }

    /**
     * Ajoute le caractère donné à l'endroit où est le curseur dans la zone de
     * saisie et met à jour le modèle.
     *
     * @param c le caractère à ajouter.
     */
    public void addChar(char c) {
        try {

            ui.getTextField().getDocument().insertString(ui.getTextField().getCaretPosition(), c + "", null);

        } catch (BadLocationException e) {
            log.warn(e);
        }

        setTextValue(ui.getTextField().getText());

    }

    /**
     * Supprime le caractère juste avant le curseur du modèle (textuel) et
     * met à jour la zone de saisie.
     */
    public void removeChar() {

        JTextField textField = ui.getTextField();

        int position = textField.getCaretPosition();
        if (position < 1) {
            if (log.isDebugEnabled()) {
                log.debug("cannot remove when caret on first position or text empty");
            }
            // on est au debut du doc, donc rien a faire
            return;
        }
        try {
            textField.getDocument().remove(position - 1, 1);
        } catch (BadLocationException ex) {
            // ne devrait jamais arrive vu qu'on a fait le controle...
            log.debug(ex);
            return;
        }
        String newText = textField.getText();
        if (log.isDebugEnabled()) {
            log.debug("text updated : " + newText);
        }
        position--;
        textField.setCaretPosition(position);

        setTextValue(newText);

    }

    public void reset() {

//        ui.getModel().setNumberValue(null);

        setTextValue("");

    }

    /**
     * Permute le signe dans la zone de saisie et
     * dans le modèle.
     */
    public void toggleSign() {

        String newValue = ui.getModel().getTextValue();

        if (newValue.startsWith("-")) {
            newValue = newValue.substring(1);
        } else {
            newValue = "-" + newValue;
        }

        setTextValue(newValue);

    }

    public void setTextValue(KeyEvent event, String newText) {
        if (event.getKeyCode() == KeyEvent.VK_D && event.isControlDown()) {
            log.debug("Reset code");
            newText = "";
        }
        setTextValue(newText);
    }

    public void setTextValue(String newText) {

        NumberEditorModel model = ui.getModel();

        boolean textValid;

        if (StringUtils.isEmpty(newText)) {

            // empty text is always valid
            textValid = true;

        } else {

            if (numberPattern != null) {

                // use given number pattern to check text
                Matcher matcher = numberPattern.matcher(newText);

                textValid = matcher.matches();

                if (!textValid && model.isCanUseSign() && "-".equals(newText)) {
                    // limit case when we have only "-"
                    textValid = true;
                }

            } else {

                // check text validity "by hand"
                //TODO
                textValid = true;

            }

        }

        if (textValid) {
            log.info(String.format("Text [%s] is valid, will set it to model", newText));
            int caretPosition = ui.getTextField().getCaretPosition();
            model.setTextValue(newText);
            if (caretPosition >= 0 && caretPosition < newText.length()) {
                ui.getTextField().setCaretPosition(caretPosition);
            }
        } else {

            String oldText = model.getTextValue();
            if (oldText == null) {
                oldText = "";
            }

            log.info(String.format("Text [%s] is not valid, will rollback to previous valid text: %s", newText, oldText));

            int caretPosition = ui.getTextField().getCaretPosition() - 1;
            ui.getTextField().setText(oldText);
            if (caretPosition >= 0 && caretPosition < oldText.length()) {
                ui.getTextField().setCaretPosition(caretPosition);
            }
        }
    }

    /**
     * Affiche ou cache la popup.
     *
     * @param newValue la nouvelle valeur de visibilité de la popup.
     */
    public void setPopupVisible(Boolean newValue) {

        if (log.isTraceEnabled()) {
            log.trace(newValue);
        }

        if (newValue == null || !newValue) {
            ui.getPopup().setVisible(false);
            return;
        }
        SwingUtilities.invokeLater(() -> {
            JComponent invoker =
                    ui.isShowPopupButton() ?
                            ui.getShowPopUpButton() :
                            ui;
            Dimension dim = ui.getPopup().getPreferredSize();
            int x = (int) (invoker.getPreferredSize().getWidth() - dim.getWidth());
            ui.getPopup().show(invoker, x, invoker.getHeight());
            ui.getTextField().requestFocusInWindow();
        });
    }

    protected void setNumberPattern(String newPattern) {

        try {
            this.numberPattern = Pattern.compile(newPattern);
        } catch (Exception e) {
            throw new IllegalStateException("Could not compute numberPattern " + newPattern + " on " + ui, e);
        }

        if (log.isInfoEnabled()) {
            log.info("init numberPattern: " + numberPattern + " on " + ui);
        }

    }

    protected void setNumberValueFromTextValue(String textValue) {

        if (ui.getModel().isNumberValueIsAdjusting()) {
            // do nothing if number value is already adjusting
            return;
        }

        Number numberValue;

        if (StringUtils.isBlank(textValue)) {

            numberValue = null;

        } else {

            numberValue = numberParserFormatter.parse(textValue);

        }

        if (log.isInfoEnabled()) {
            log.info("Set numberValue " + numberValue + " from textValue " + textValue);
        }
        ui.getModel().setNumberValue(numberValue);
    }

    protected void setTextValueFromNumberValue(Number numberValue) {

        if (ui.getModel().isTextValueIsAdjusting()) {
            // do nothing if text value is already adjusting
            return;
        }

        String textValue = numberParserFormatter.format(numberValue);

        if (log.isInfoEnabled()) {
            log.info("Set textValue " + textValue + " from numberValue " + numberValue);
        }
        ui.getModel().setTextValue(textValue);

    }

    protected void validate() {

        setPopupVisible(false);
        // fire validate property (to be able to notify listeners)
        ui.firePropertyChange(VALIDATE_PROPERTY, null, true);
    }

    interface NumberParserFormatter {

        Number parse(String textValue);

        String format(Number numberValue);

    }

    protected class PopupListener extends MouseAdapter {

        @Override
        public void mousePressed(MouseEvent e) {
            maybeShowPopup(e);
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            maybeShowPopup(e);
        }

        protected void maybeShowPopup(MouseEvent e) {
            if (!e.isPopupTrigger()) {
                return;
            }
            if (ui.isAutoPopup()) {
                if (ui.isPopupVisible()) {
                    if (!ui.getPopup().isVisible()) {
                        setPopupVisible(true);
                    }
                    // popup already visible

                } else {
                    // set popup auto
                    ui.setPopupVisible(true);

                }
            } else {
                if (ui.isPopupVisible()) {
                    setPopupVisible(true);
                }
            }
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy