001package jmri.jmrit.symbolicprog;
002
003import java.awt.Color;
004import java.awt.Component;
005import java.awt.event.ActionEvent;
006import java.awt.event.ActionListener;
007import java.awt.event.FocusEvent;
008import java.awt.event.FocusListener;
009import java.util.ArrayList;
010import java.util.HashMap;
011import java.util.Hashtable;
012import javax.swing.JLabel;
013import javax.swing.JSlider;
014import javax.swing.JTextField;
015import javax.swing.text.Document;
016
017import org.slf4j.Logger;
018import org.slf4j.LoggerFactory;
019
020/**
021 * Decimal representation of a value.
022 * <br>
023 * The {@code mask} attribute represents the part of the value that's present in
024 * the CV.
025 * <br>
026 * Optional attributes {@code factor} and {@code offset} are applied when going
027 * <i>from</i> the variable value <i>to</i> the CV values, or vice-versa:
028 * <pre>
029 * Value to put in CVs = ((value in text field) -{@code offset})/{@code factor}
030 * Value to put in text field = ((value in CVs) *{@code factor}) +{@code offset}
031 * </pre> *
032 *
033 * @author Bob Jacobsen Copyright (C) 2001, 2022
034 */
035public class DecVariableValue extends VariableValue
036        implements ActionListener, FocusListener {
037
038    public DecVariableValue(String name, String comment, String cvName, boolean readOnly, boolean infoOnly,
039                            boolean writeOnly, boolean opsOnly, String cvNum, String mask, int minVal, int maxVal,
040                            HashMap<String, CvValue> v, JLabel status, String stdname) {
041        this(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, minVal, maxVal,
042                v, status, stdname, 0, 1);
043    }
044
045    public DecVariableValue(String name, String comment, String cvName, boolean readOnly, boolean infoOnly,
046                            boolean writeOnly, boolean opsOnly, String cvNum, String mask, int minVal, int maxVal,
047            HashMap<String, CvValue> v, JLabel status, String stdname, int offset, int factor) {
048        super(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, v, status, stdname);
049        _maxVal = maxVal;
050        _minVal = minVal;
051        _offset = offset;
052        _factor = factor;
053        _value = new JTextField("0", fieldLength());
054        _value.getAccessibleContext().setAccessibleName(label());
055        _defaultColor = _value.getBackground();
056        _value.setBackground(ValueState.UNKNOWN.getColor());
057        // connect to the JTextField value, cv
058        _value.addActionListener(this);
059        _value.addFocusListener(this);
060        CvValue cv = _cvMap.get(getCvNum());
061        cv.addPropertyChangeListener(this);
062        cv.setState(ValueState.FROMFILE);
063        simplifyMask();
064    }
065
066    @Override
067    public void setToolTipText(String t) {
068        super.setToolTipText(t);   // do default stuff
069        _value.setToolTipText(t);  // set our value
070    }
071
072    int _maxVal;
073    int _minVal;
074    int _offset;
075    int _factor;
076
077    int fieldLength() {
078        if (_maxVal <= 255) {
079            return 3;
080        }
081        return (int) Math.ceil(Math.log10(_maxVal)) + 1;
082    }
083
084    @Override
085    public CvValue[] usesCVs() {
086        return new CvValue[]{_cvMap.get(getCvNum())};
087    }
088
089    @Override
090    public Object rangeVal() {
091        return "Decimal: " + _minVal + " - " + _maxVal;
092    }
093
094    String oldContents = "";
095
096    void enterField() {
097        oldContents = _value.getText();
098    }
099
100    int textToValue(String s) {
101        return (Integer.parseInt(s));
102    }
103
104    String valueToText(int v) {
105        return (Integer.toString(v));
106    }
107
108    void exitField() {
109        if (_value == null) {
110            // There's no value Object yet, so just ignore & exit
111            return;
112        }
113        // what to do for the case where _value != null?
114        if (!_value.getText().equals("")) {
115            // there may be a lost focus event left in the queue when disposed, so protect
116            if (!oldContents.equals(_value.getText())) {
117                try {
118                    int newVal = textToValue(_value.getText());
119                    int oldVal = textToValue(oldContents);
120                    if (newVal < _minVal || newVal > _maxVal) {
121                        _value.setText(oldContents);
122                    } else {
123                        updatedTextField();
124                        prop.firePropertyChange("Value", oldVal, newVal);
125                    }
126                } catch (java.lang.NumberFormatException ex) {
127                    _value.setText(oldContents);
128                }
129            }
130        } else {
131            // As the user has left the contents blank, we shall re-instate the old value as,
132            // when a write operation to decoder is performed, the cv remains the same value.
133            _value.setText(oldContents);
134        }
135    }
136
137    /**
138     * Invoked when a permanent change to the JTextField has been made. Note
139     * that this does _not_ notify property listeners; that should be done by
140     * the invoker, who may or may not know what the old value was. Can be
141     * overridden in subclasses that want to display the value differently.
142     */
143    @Override
144    void updatedTextField() {
145        log.debug("updatedTextField");
146        // called for new values - set the CV as needed
147        CvValue cv = _cvMap.get(getCvNum());
148        // compute new cv value by combining old and request
149        int oldCvVal = cv.getValue();
150        int newVal;
151        try {
152            newVal = textToValue(_value.getText());
153        } catch (java.lang.NumberFormatException ex) {
154            newVal = 0;
155        }
156        int transfer = Math.max(newVal - _offset, 0); // prevent negative values, especially in tests outside UI
157        if (_factor != 0) {
158            transfer = transfer / _factor;
159        } else {
160            // ignore division
161            log.error("Variable param 'factor' = 0 not valid; Decoder definition needs correction");
162        }
163        int newCvVal = setValueInCV(oldCvVal, transfer, getMask(), _maxVal);
164        log.debug("newVal={} transfer={} newCvVal ={}", newVal, transfer, newCvVal);
165        if (oldCvVal != newCvVal) {
166            cv.setValue(newCvVal);
167        }
168    }
169
170    /**
171     * ActionListener implementations
172     */
173    @Override
174    public void actionPerformed(ActionEvent e) {
175        log.debug("actionPerformed");
176        try {
177            int newVal = textToValue(_value.getText());
178            if (newVal < _minVal || newVal > _maxVal) {
179                _value.setText(oldContents);
180            } else {
181                updatedTextField();
182                prop.firePropertyChange("Value", null, newVal);
183            }
184        } catch (java.lang.NumberFormatException ex) {
185            _value.setText(oldContents);
186        }
187    }
188
189    /**
190     * FocusListener implementations
191     */
192    @Override
193    public void focusGained(FocusEvent e) {
194        log.debug("focusGained");
195        enterField();
196    }
197
198    @Override
199    public void focusLost(FocusEvent e) {
200        log.debug("focusLost");
201        exitField();
202    }
203
204    // to complete this class, fill in the routines to handle "Value" parameter
205    // and to read/write/hear parameter changes.
206    @Override
207    public String getValueString() {
208        return _value.getText();
209    }
210
211    @Override
212    public void setIntValue(int i) {
213        setValue(i);
214    }
215
216    @Override
217    public int getIntValue() {
218        return textToValue(_value.getText());
219    }
220
221    @Override
222    public Object getValueObject() {
223        return Integer.valueOf(_value.getText());
224    }
225
226    @Override
227    public Component getCommonRep() {
228        if (getReadOnly()) {
229            JLabel r = new JLabel(_value.getText());
230            reps.add(r);
231            updateRepresentation(r);
232            return r;
233        } else {
234            return _value;
235        }
236    }
237
238    @Override
239    public void setAvailable(boolean a) {
240        _value.setVisible(a);
241        for (Component c : reps) {
242            c.setVisible(a);
243        }
244        super.setAvailable(a);
245    }
246
247    java.util.List<Component> reps = new java.util.ArrayList<>();
248
249    @Override
250    public Component getNewRep(String format) {
251        switch (format) {
252            case "vslider": {
253                DecVarSlider b = new DecVarSlider(this, _minVal, _maxVal);
254                b.setOrientation(JSlider.VERTICAL);
255                sliders.add(b);
256                reps.add(b);
257                updateRepresentation(b);
258                return b;
259            }
260            case "hslider": {
261                DecVarSlider b = new DecVarSlider(this, _minVal, _maxVal);
262                b.setOrientation(JSlider.HORIZONTAL);
263                sliders.add(b);
264                reps.add(b);
265                updateRepresentation(b);
266                return b;
267            }
268            case "hslider-percent": {
269                DecVarSlider b = new DecVarSlider(this, _minVal, _maxVal);
270                b.setOrientation(JSlider.HORIZONTAL);
271                if (_maxVal > 20) {
272                    b.setMajorTickSpacing(_maxVal / 2);
273                    b.setMinorTickSpacing((_maxVal + 1) / 8);
274                } else {
275                    b.setMajorTickSpacing(5);
276                    b.setMinorTickSpacing(1); // because JSlider does not SnapToValue
277                    b.setSnapToTicks(true);   // like it should, we fake it here
278                }
279                b.setSize(b.getWidth(), 28);
280                Hashtable<Integer, JLabel> labelTable = new Hashtable<>();
281                labelTable.put(0, new JLabel("0%"));
282                if (_maxVal == 63) {   // this if for the QSI mute level, not very universal, needs work
283                    labelTable.put(_maxVal / 2, new JLabel("25%"));
284                    labelTable.put(_maxVal, new JLabel("50%"));
285                } else {
286                    labelTable.put(_maxVal / 2, new JLabel("50%"));
287                    labelTable.put(_maxVal, new JLabel("100%"));
288                }
289                b.setLabelTable(labelTable);
290                b.setPaintTicks(true);
291                b.setPaintLabels(true);
292                sliders.add(b);
293                updateRepresentation(b);
294                if (!getAvailable()) {
295                    b.setVisible(false);
296                }
297                return b;
298            }
299            default:
300                JTextField value = new VarTextField(_value.getDocument(), _value.getText(), fieldLength(), this);
301                if (getReadOnly() || getInfoOnly()) {
302                    value.setEditable(false);
303                }
304                reps.add(value);
305                updateRepresentation(value);
306                return value;
307        }
308    }
309
310    ArrayList<DecVarSlider> sliders = new ArrayList<>();
311
312    /**
313     * Set a new value in the variable (text box), including notification as needed.
314     * <p>
315     * This does the conversion from string to int, so it's the place where
316     * formatting needs to be applied.
317     * @param value new value.
318     */
319    public void setValue(int value) {
320        int oldVal;
321        try {
322            oldVal = textToValue(_value.getText());
323        } catch (java.lang.NumberFormatException ex) {
324            oldVal = -999;
325        }
326        if (value < _minVal) value = _minVal;
327        if (value > _maxVal) value = _maxVal;
328        log.debug("setValue with new value {} old value {}", value, oldVal);
329        if (oldVal != value) {
330            _value.setText(valueToText(value));
331            updatedTextField();
332            prop.firePropertyChange("Value", Integer.valueOf(oldVal), Integer.valueOf(value));
333        }
334    }
335
336    Color _defaultColor;
337
338    // implement an abstract member to set colors
339    Color getDefaultColor() {
340        return _defaultColor;
341    }
342
343    Color getColor() {
344        return _value.getBackground();
345    }
346
347    @Override
348    void setColor(Color c) {
349        if (c != null) {
350            _value.setBackground(c);
351        } else {
352            _value.setBackground(_defaultColor);
353        }
354        // prop.firePropertyChange("Value", null, null);
355    }
356
357    /**
358     * Notify the connected CVs of a state change from above
359     *
360     */
361    @Override
362    public void setCvState(ValueState state) {
363        _cvMap.get(getCvNum()).setState(state);
364    }
365
366    @Override
367    public boolean isChanged() {
368        CvValue cv = _cvMap.get(getCvNum());
369        log.debug("isChanged for {} state {}", getCvNum(), cv.getState());
370        return considerChanged(cv);
371    }
372
373    @Override
374    public void readChanges() {
375        if (isChanged()) {
376            readAll();
377        }
378    }
379
380    @Override
381    public void writeChanges() {
382        if (isChanged()) {
383            writeAll();
384        }
385    }
386
387    @Override
388    public void readAll() {
389        setToRead(false);
390        setBusy(true);  // will be reset when value changes
391        //super.setState(READ);
392        _cvMap.get(getCvNum()).read(_status);
393    }
394
395    @Override
396    public void writeAll() {
397        setToWrite(false);
398        if (getReadOnly()) {
399            log.error("unexpected write operation when readOnly is set");
400        }
401        setBusy(true);  // will be reset when value changes
402        _cvMap.get(getCvNum()).write(_status);
403    }
404
405    // handle incoming parameter notification
406    @Override
407    public void propertyChange(java.beans.PropertyChangeEvent e) {
408        // notification from CV; check for Value being changed
409        if (log.isDebugEnabled()) {
410            log.debug("Property changed: {}", e.getPropertyName());
411        }
412        if (e.getPropertyName().equals("Busy")) {
413            if (e.getNewValue().equals(Boolean.FALSE)) {
414                setToRead(false);
415                setToWrite(false);  // some programming operation just finished
416                setBusy(false);
417            }
418        } else if (e.getPropertyName().equals("State")) {
419            CvValue cv = _cvMap.get(getCvNum());
420            if (cv.getState() == ValueState.STORED) {
421                setToWrite(false);
422            }
423            if (cv.getState() == ValueState.READ) {
424                setToRead(false);
425            }
426            setState(cv.getState());
427        } else if (e.getPropertyName().equals("Value")) {
428            // update value of Variable
429            CvValue cv = _cvMap.get(getCvNum());
430            int transfer = getValueInCV(cv.getValue(), getMask(), _maxVal);
431            int newVal = (transfer * _factor) + _offset;
432            setValue(newVal);  // check for duplicate done inside setValue
433        }
434    }
435
436    // stored value, read-only Value
437    JTextField _value;
438
439    /* Internal class extends a JTextField so that its color is consistent with
440     * an underlying variable
441     *
442     * @author   Bob Jacobsen   Copyright (C) 2001
443     */
444    public class VarTextField extends JTextField {
445
446        VarTextField(Document doc, String text, int col, DecVariableValue var) {
447            super(doc, text, col);
448            _var = var;
449            // get the original color right
450            setBackground(_var._value.getBackground());
451            // listen for changes to ourself
452            addActionListener(this::thisActionPerformed);
453            addFocusListener(new java.awt.event.FocusListener() {
454                @Override
455                public void focusGained(FocusEvent e) {
456                    log.debug("focusGained");
457                    enterField();
458                }
459
460                @Override
461                public void focusLost(FocusEvent e) {
462                    log.debug("focusLost");
463                    exitField();
464                }
465            });
466            // listen for changes to original state
467            _var.addPropertyChangeListener(this::originalPropertyChanged);
468        }
469
470        DecVariableValue _var;
471
472        void thisActionPerformed(java.awt.event.ActionEvent e) {
473            // tell original
474            _var.actionPerformed(e);
475        }
476
477        void originalPropertyChanged(java.beans.PropertyChangeEvent e) {
478            // update this color from original state
479            if (e.getPropertyName().equals("State")) {
480                setBackground(_var._value.getBackground());
481            }
482        }
483
484    }
485
486    // clean up connections when done
487    @Override
488    public void dispose() {
489        log.debug("dispose");
490        if (_value != null) {
491            _value.removeActionListener(this);
492        }
493        _cvMap.get(getCvNum()).removePropertyChangeListener(this);
494
495        _value = null;
496        // do something about the VarTextField
497    }
498
499    // initialize logging
500    private final static Logger log = LoggerFactory.getLogger(DecVariableValue.class);
501
502}