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

eu.limetri.client.mapviewer.nb.jxmap.map.CustomRangePanel Maven / Gradle / Ivy

There is a newer version: 1.4.4
Show newest version
/**
 * Copyright (C) 2008-2012 AgroSense Foundation.
 *
 * AgroSense is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * There are special exceptions to the terms and conditions of the GPLv3 as it is applied to
 * this software, see the FLOSS License Exception
 * .
 *
 * AgroSense 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 Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with AgroSense.  If not, see .
 */

package eu.limetri.client.mapviewer.nb.jxmap.map;

import eu.limetri.client.mapviewer.api.RangedLegendPalette;
import eu.limetri.client.mapviewer.api.RangedLegendPalette.Range;
import java.awt.Color;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.util.Collection;
import javax.swing.ButtonGroup;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.SwingUtilities;
import javax.swing.text.DefaultFormatter;
import net.miginfocom.swing.MigLayout;
import org.openide.util.NbBundle;

/**
 *
 * @author johan
 */
@NbBundle.Messages({
    "range_settings_panel.name=Range settings",
    "range_settings_panel.label data range=Data range",
    "range_settings_panel.label custom range=Custom range",
})
public class CustomRangePanel extends JPanel {
    private final RangedLegendPalette palette;
    private final Range customRange;
    
    private JRadioButton dataOption;
    private RangeRow dataRangeRow;
    private JRadioButton customOption;
    private RangeRow customRangeRow;
    
    public CustomRangePanel(RangedLegendPalette palette) {
        this.palette = palette;
        this.customRange = new Range<>(palette.getMinValue(), palette.getMaxValue());
        
        initComponents();
    }
    
    private void initComponents() {
        setLayout(new MigLayout("wrap 2"));

        // 1) data range, show current values read-only
        // 2) create custom range
        // 3) maybe later: select custom ranges from combobox; 
        //      possible sources: favorites, interchangeable ranges (similar to palette behavior), measurement types, ...
                
        dataOption = new JRadioButton(Bundle.range_settings_panel_label_data_range());
        dataOption.setSelected(!palette.usesCustomRange());
        dataRangeRow = new RangeRow(palette.getDataRange(), true);
        
        customOption = new JRadioButton(Bundle.range_settings_panel_label_custom_range());
        customOption.setSelected(palette.usesCustomRange());
        customRangeRow = new RangeRow(customRange, false);
        
        // select the custom option when one of the custom range fields gains focus:
        customRangeRow.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent fe) {
                if (!fe.isTemporary()) customOption.setSelected(true);
            }
        });
        
        ButtonGroup group = new ButtonGroup();
        group.add(dataOption);
        group.add(customOption);
        
        add(dataOption, "top");
        add(dataRangeRow);
        add(customOption, "top");
        add(customRangeRow);
        
        // spacer, part of custom range message was cut off
        add(new JLabel(), "span");
    }
    
    
    //bind to OK button
    protected void apply(Collection> palettes) {
        if (dataOption.isSelected()) {
            for (RangedLegendPalette p : palettes) {
                p.applyDataRange();
            }
        } else if (customOption.isSelected()) {
            for (RangedLegendPalette p : palettes) {
                p.setRange(customRange);
            }
        }
    }
    
    private static Format createFormat() {
        return ParseEntireString.of(new DecimalFormat());
    }

    @NbBundle.Messages({
        "RangeRow error min invalid=Lower bound is not a valid number",
        "RangeRow error max invalid=Upper bound is not a valid number",
        "RangeRow error min and max invalid=Neither lower or upper bound is a valid number ",
        "RangeRow error range invalid=Lower bound is larger than upper bound",
    })
    private class RangeRow extends JPanel {
        private final boolean readOnly;
        private final JFormattedTextField minField = new JFormattedTextField(createFormat());
        private final JFormattedTextField maxField = new JFormattedTextField(createFormat());
        private final JLabel msgLabel = new JLabel("");
        private final Range range;
        
        private boolean minValid = true;
        private boolean maxValid = true;
        private boolean rangeValid = true;

        public RangeRow(Range range, boolean readonly) {
            this.range = range;
            this.readOnly = readonly;
            
            initComponents();
            bind();
        }
        
        private void initComponents() {
            setLayout(new MigLayout("wrap 3, insets 0 0 0 0"));
            
            initTexField(minField, range.getMin());
            initTexField(maxField, range.getMax());

            msgLabel.setForeground(Color.red);
            msgLabel.setVisible(false);
            
            add(minField);
            add(new JLabel(" - "));
            add(maxField);
            
            //msg on its own row:
            add(msgLabel, "span");
        }
        
        private void initTexField(final JFormattedTextField field, Number value) {
            field.setColumns(10);
            field.setValue(toDouble(value) / palette.getScale());
            
            if (readOnly) {
                field.setEnabled(false);
            } else {
                //don't automatically revert invalid values (show msg instead):
                field.setFocusLostBehavior(JFormattedTextField.COMMIT);
                
                //post all valid edits: allows for applying all validations while still typing instead of on focus loss
                ((DefaultFormatter)field.getFormatter()).setCommitsOnValidEdit(true);
                
                //caret fix: default behavior puts it before 1st char, no matter where you click
                field.addFocusListener(new FocusAdapter() {
                    @Override
                    public void focusGained(FocusEvent e) {
                        final int dot = field.getCaret().getDot();
                        SwingUtilities.invokeLater(new Runnable() {
                            @Override
                            public void run() {
                                field.getCaret().setDot(dot);
                            }
                        });
                    }
                });
            }
        }
        
        @Override
        public void addFocusListener(FocusListener fl) {
            //delegate to focusable components (all text fields):
            minField.addFocusListener(fl);
            maxField.addFocusListener(fl);
        }
        
        private void bind() {
            if (!readOnly) {
                //"value" won't fire when changing invalid text back to the last valid value
                minField.addPropertyChangeListener("value", new PropertyChangeListener() {
                    @Override
                    public void propertyChange(PropertyChangeEvent pce) {
                        Number nMin = (Number) pce.getNewValue();
                        Number nMax = (Number) maxField.getValue();
                        //compare with other value from field, not from range, and post both:
                        // e.g. while editing the min value, the current max value showing might not be in 
                        // the range object yet because of a previous range validation failure.
                        postIfValid(nMin, nMax);
                    }
                });
                maxField.addPropertyChangeListener("value", new PropertyChangeListener() {
                    @Override
                    public void propertyChange(PropertyChangeEvent pce) {
                        Number nMax = (Number) pce.getNewValue();
                        Number nMin = (Number) minField.getValue();
                        postIfValid(nMin, nMax);
                    }
                });
                
                //"editValid" will fire before "value"
                minField.addPropertyChangeListener("editValid", new PropertyChangeListener() {
                    @Override
                    public void propertyChange(PropertyChangeEvent pce) {
                        minValid = (Boolean) pce.getNewValue();
                        updateMsg();
                    }
                });
                maxField.addPropertyChangeListener("editValid", new PropertyChangeListener() {
                    @Override
                    public void propertyChange(PropertyChangeEvent pce) {
                        maxValid = (Boolean) pce.getNewValue();
                        updateMsg();
                    }
                });
            }
        }
        
        private void postIfValid(Number nMin, Number nMax) {
            // the textfields post their text to their value if parsing succeeds;
            // we'll post both values to the range if the combination is valid.
            double min = toDouble(nMin) * palette.getScale();
            double max = toDouble(nMax) * palette.getScale();

            rangeValid = !(min > max);
            updateMsg();
            
            if(isStateValid()) {
                range.setMin(min);
                range.setMax(max);
            }
        }
        
        private boolean isStateValid() {
            return minValid && maxValid && rangeValid;
        }
        
        private void updateMsg() {
            if (!(minValid || maxValid)) setMsg(Bundle.RangeRow_error_min_and_max_invalid());
            else if (!minValid) setMsg(Bundle.RangeRow_error_min_invalid());
            else if (!maxValid) setMsg(Bundle.RangeRow_error_max_invalid());
            else if (!rangeValid) setMsg(Bundle.RangeRow_error_range_invalid());
            else clearMsg();                          
        }
        
        private double toDouble(Number n) {
            return n == null ? 0.0 : n.doubleValue();
        }
        
        private void setMsg(String msg) {
            msgLabel.setText(msg);
            msgLabel.setVisible(true);
        }
        
        private void clearMsg() {
            msgLabel.setVisible(false);
        }
        
    }
        
    //Format decorator that ensures the entire source string is used in parsing:
    // e.g. NumberFormat allows trailing characters that won't cause an error ("12bla" is ok, "bla12" is not)
    // and which (in case of a JFormattedTextField) will disappear again once focus is lost.
    //
    //TODO: extract to util library
    private static class ParseEntireString extends Format {
        private final Format original;

        public ParseEntireString(Format original) {
            this.original = original;
        }
        
        public static Format of(Format original) {
            return new ParseEntireString(original);
        }

        @Override
        public StringBuffer format(Object o, StringBuffer sb, FieldPosition fp) {
            return original.format(o, sb, fp);
        }

        @Override
        public Object parseObject(String string, ParsePosition pp) {
            int initialIndex = pp.getIndex();
            Object result = original.parseObject(string, pp);
            if (result != null && pp.getIndex() < string.length()) {
                int errorIndex = pp.getIndex();
                pp.setIndex(initialIndex);
                pp.setErrorIndex(errorIndex);
                return null;
            }
            return result;
        }
        
    }
    
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy