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
157        newVal = newVal - _offset;
158        if (_factor != 0) {
159            newVal = newVal / _factor;
160        } else {
161            // ignore division
162            log.error("Variable param 'factor' = 0 not valid; Decoder definition needs correction");
163        }
164        
165        int newCvVal = setValueInCV(oldCvVal, newVal, getMask(), _maxVal);
166        log.debug("newVal={} newCvVal ={}", newVal, newCvVal);
167        if (oldCvVal != newCvVal) {
168            cv.setValue(newCvVal);
169        }
170    }
171
172    /**
173     * ActionListener implementations
174     */
175    @Override
176    public void actionPerformed(ActionEvent e) {
177        log.debug("actionPerformed");
178        try {
179            int newVal = textToValue(_value.getText());
180            if (newVal < _minVal || newVal > _maxVal) {
181                _value.setText(oldContents);
182            } else {
183                updatedTextField();
184                prop.firePropertyChange("Value", null, newVal);
185            }
186        } catch (java.lang.NumberFormatException ex) {
187            _value.setText(oldContents);
188        }
189    }
190
191    /**
192     * FocusListener implementations
193     */
194    @Override
195    public void focusGained(FocusEvent e) {
196        log.debug("focusGained");
197        enterField();
198    }
199
200    @Override
201    public void focusLost(FocusEvent e) {
202        log.debug("focusLost");
203        exitField();
204    }
205
206    // to complete this class, fill in the routines to handle "Value" parameter
207    // and to read/write/hear parameter changes.
208    @Override
209    public String getValueString() {
210        return _value.getText();
211    }
212
213    @Override
214    public void setIntValue(int i) {
215        setValue(i);
216    }
217
218    @Override
219    public int getIntValue() {
220        return textToValue(_value.getText());
221    }
222
223    @Override
224    public Object getValueObject() {
225        return Integer.valueOf(_value.getText());
226    }
227
228    @Override
229    public Component getCommonRep() {
230        if (getReadOnly()) {
231            JLabel r = new JLabel(_value.getText());
232            reps.add(r);
233            updateRepresentation(r);
234            return r;
235        } else {
236            return _value;
237        }
238    }
239
240    @Override
241    public void setAvailable(boolean a) {
242        _value.setVisible(a);
243        for (Component c : reps) {
244            c.setVisible(a);
245        }
246        super.setAvailable(a);
247    }
248
249    java.util.List<Component> reps = new java.util.ArrayList<>();
250
251    @Override
252    public Component getNewRep(String format) {
253        switch (format) {
254            case "vslider": {
255                DecVarSlider b = new DecVarSlider(this, _minVal, _maxVal);
256                b.setOrientation(JSlider.VERTICAL);
257                sliders.add(b);
258                reps.add(b);
259                updateRepresentation(b);
260                return b;
261            }
262            case "hslider": {
263                DecVarSlider b = new DecVarSlider(this, _minVal, _maxVal);
264                b.setOrientation(JSlider.HORIZONTAL);
265                sliders.add(b);
266                reps.add(b);
267                updateRepresentation(b);
268                return b;
269            }
270            case "hslider-percent": {
271                DecVarSlider b = new DecVarSlider(this, _minVal, _maxVal);
272                b.setOrientation(JSlider.HORIZONTAL);
273                if (_maxVal > 20) {
274                    b.setMajorTickSpacing(_maxVal / 2);
275                    b.setMinorTickSpacing((_maxVal + 1) / 8);
276                } else {
277                    b.setMajorTickSpacing(5);
278                    b.setMinorTickSpacing(1); // because JSlider does not SnapToValue
279                    b.setSnapToTicks(true);   // like it should, we fake it here
280                }
281                b.setSize(b.getWidth(), 28);
282                Hashtable<Integer, JLabel> labelTable = new Hashtable<>();
283                labelTable.put(0, new JLabel("0%"));
284                if (_maxVal == 63) {   // this if for the QSI mute level, not very universal, needs work
285                    labelTable.put(_maxVal / 2, new JLabel("25%"));
286                    labelTable.put(_maxVal, new JLabel("50%"));
287                } else {
288                    labelTable.put(_maxVal / 2, new JLabel("50%"));
289                    labelTable.put(_maxVal, new JLabel("100%"));
290                }
291                b.setLabelTable(labelTable);
292                b.setPaintTicks(true);
293                b.setPaintLabels(true);
294                sliders.add(b);
295                updateRepresentation(b);
296                if (!getAvailable()) {
297                    b.setVisible(false);
298                }
299                return b;
300            }
301            default:
302                JTextField value = new VarTextField(_value.getDocument(), _value.getText(), fieldLength(), this);
303                if (getReadOnly() || getInfoOnly()) {
304                    value.setEditable(false);
305                }
306                reps.add(value);
307                updateRepresentation(value);
308                return value;
309        }
310    }
311
312    ArrayList<DecVarSlider> sliders = new ArrayList<>();
313
314    /**
315     * Set a new value in the variable (text box), including notification as needed.
316     * <p>
317     * This does the conversion from string to int, so it's the place where
318     * formatting needs to be applied.
319     * @param value new value.
320     */
321    public void setValue(int value) {
322        int oldVal;
323        try {
324            oldVal = textToValue(_value.getText());
325        } catch (java.lang.NumberFormatException ex) {
326            oldVal = -999;
327        }
328        
329        if (value < _minVal) value = _minVal;
330        if (value > _maxVal) value = _maxVal;
331        log.debug("setValue with new value {} old value {}", value, oldVal);
332        if (oldVal != value) {
333            _value.setText(valueToText(value));
334            updatedTextField();
335            prop.firePropertyChange("Value", Integer.valueOf(oldVal), Integer.valueOf(value));
336        }
337    }
338
339    Color _defaultColor;
340
341    // implement an abstract member to set colors
342    Color getDefaultColor() {
343        return _defaultColor;
344    }
345
346    Color getColor() {
347        return _value.getBackground();
348    }
349
350    @Override
351    void setColor(Color c) {
352        if (c != null) {
353            _value.setBackground(c);
354        } else {
355            _value.setBackground(_defaultColor);
356        }
357        // prop.firePropertyChange("Value", null, null);
358    }
359
360    /**
361     * Notify the connected CVs of a state change from above
362     *
363     */
364    @Override
365    public void setCvState(ValueState state) {
366        _cvMap.get(getCvNum()).setState(state);
367    }
368
369    @Override
370    public boolean isChanged() {
371        CvValue cv = _cvMap.get(getCvNum());
372        log.debug("isChanged for {} state {}", getCvNum(), cv.getState());
373        return considerChanged(cv);
374    }
375
376    @Override
377    public void readChanges() {
378        if (isChanged()) {
379            readAll();
380        }
381    }
382
383    @Override
384    public void writeChanges() {
385        if (isChanged()) {
386            writeAll();
387        }
388    }
389
390    @Override
391    public void readAll() {
392        setToRead(false);
393        setBusy(true);  // will be reset when value changes
394        //super.setState(READ);
395        _cvMap.get(getCvNum()).read(_status);
396    }
397
398    @Override
399    public void writeAll() {
400        setToWrite(false);
401        if (getReadOnly()) {
402            log.error("unexpected write operation when readOnly is set");
403        }
404        setBusy(true);  // will be reset when value changes
405        _cvMap.get(getCvNum()).write(_status);
406    }
407
408    // handle incoming parameter notification
409    @Override
410    public void propertyChange(java.beans.PropertyChangeEvent e) {
411        // notification from CV; check for Value being changed
412        if (log.isDebugEnabled()) {
413            log.debug("Property changed: {}", e.getPropertyName());
414        }
415        if (e.getPropertyName().equals("Busy")) {
416            if (e.getNewValue().equals(Boolean.FALSE)) {
417                setToRead(false);
418                setToWrite(false);  // some programming operation just finished
419                setBusy(false);
420            }
421        } else if (e.getPropertyName().equals("State")) {
422            CvValue cv = _cvMap.get(getCvNum());
423            if (cv.getState() == ValueState.STORED) {
424                setToWrite(false);
425            }
426            if (cv.getState() == ValueState.READ) {
427                setToRead(false);
428            }
429            setState(cv.getState());
430        } else if (e.getPropertyName().equals("Value")) {
431            // update value of Variable
432            CvValue cv = _cvMap.get(getCvNum());
433            int transfer = getValueInCV(cv.getValue(), getMask(), _maxVal);
434            
435            int newVal = (transfer * _factor) + _offset;
436            
437            // handle possible negative value
438            if (_minVal < 0 && newVal > _maxVal) {
439                // here a 2's-complement variable, find the sign bit in the value
440                int signBit = signBit(getMask());
441                // sign extend the value
442                newVal = (newVal ^ signBit) - signBit;
443            }
444           
445            setValue(newVal);  // check for duplicate done inside setValue
446        }
447    }
448
449    // find the sign bit for a masked field
450    // e.g. sign bit of XXXXVVVV is 0b00001000
451    // and  sign bit of XXVVVXXX is 0b00000100
452    int signBit(String mask) {
453        int shift = offsetVal(mask);
454        int oneBits = maskValAsInt(mask)>>shift;
455        int firstBit = 31 - Integer.numberOfLeadingZeros(oneBits);
456        return 1<<firstBit;
457        
458    }
459    
460    
461    // stored value, read-only Value
462    JTextField _value;
463
464    /* Internal class extends a JTextField so that its color is consistent with
465     * an underlying variable
466     *
467     * @author   Bob Jacobsen   Copyright (C) 2001
468     */
469    public class VarTextField extends JTextField {
470
471        VarTextField(Document doc, String text, int col, DecVariableValue var) {
472            super(doc, text, col);
473            _var = var;
474            // get the original color right
475            setBackground(_var._value.getBackground());
476            // listen for changes to ourself
477            addActionListener(this::thisActionPerformed);
478            addFocusListener(new java.awt.event.FocusListener() {
479                @Override
480                public void focusGained(FocusEvent e) {
481                    log.debug("focusGained");
482                    enterField();
483                }
484
485                @Override
486                public void focusLost(FocusEvent e) {
487                    log.debug("focusLost");
488                    exitField();
489                }
490            });
491            // listen for changes to original state
492            _var.addPropertyChangeListener(this::originalPropertyChanged);
493        }
494
495        DecVariableValue _var;
496
497        void thisActionPerformed(java.awt.event.ActionEvent e) {
498            // tell original
499            _var.actionPerformed(e);
500        }
501
502        void originalPropertyChanged(java.beans.PropertyChangeEvent e) {
503            // update this color from original state
504            if (e.getPropertyName().equals("State")) {
505                setBackground(_var._value.getBackground());
506            }
507        }
508
509    }
510
511    // clean up connections when done
512    @Override
513    public void dispose() {
514        log.debug("dispose");
515        if (_value != null) {
516            _value.removeActionListener(this);
517        }
518        _cvMap.get(getCvNum()).removePropertyChangeListener(this);
519
520        _value = null;
521        // do something about the VarTextField
522    }
523
524    // initialize logging
525    private final static Logger log = LoggerFactory.getLogger(DecVariableValue.class);
526
527}