001package jmri.jmrit.symbolicprog;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.awt.Color;
005import java.awt.Component;
006import java.awt.event.ActionEvent;
007import java.awt.event.ActionListener;
008import java.awt.event.FocusEvent;
009import java.awt.event.FocusListener;
010import java.util.ArrayList;
011import java.util.HashMap;
012import java.util.List;
013import javax.swing.JLabel;
014import javax.swing.JTextField;
015import javax.swing.text.Document;
016import jmri.util.CvUtil;
017import org.slf4j.Logger;
018import org.slf4j.LoggerFactory;
019
020/**
021 * Extends VariableValue to represent a variable split across multiple CVs.
022 * <br>
023 * The {@code mask} attribute represents the part of the value that's present in
024 * each CV; higher-order bits are loaded to subsequent CVs.<br>
025 * It is possible to assign a specific mask for each CV by providing a space
026 * separated list of masks, starting with the lowest, and matching the order of
027 * CVs
028 * <br><br>
029 * The original use was for addresses of stationary (accessory) decoders.
030 * <br>
031 * The original version only allowed two CVs, with the second CV specified by
032 * the attributes {@code highCV} and {@code upperMask}.
033 * <br><br>
034 * The preferred technique is now to specify all CVs in the {@code CV} attribute
035 * alone, as documented at {@link CvUtil#expandCvList expandCvList(String)}.
036 * <br><br>
037 * Optional attributes {@code factor} and {@code offset} are applied when going
038 * <i>from</i> the variable value <i>to</i> the CV values, or vice-versa:
039 * <pre>
040 * Value to put in CVs = ((value in text field) -{@code offset})/{@code factor}
041 * Value to put in text field = ((value in CVs) *{@code factor}) +{@code offset}
042 * </pre>
043 *
044 * @author Bob Jacobsen Copyright (C) 2002, 2003, 2004, 2013
045 * @author Dave Heap Copyright (C) 2016, 2019
046 * @author Egbert Broerse Copyright (C) 2020
047 */
048public class SplitVariableValue extends VariableValue
049        implements ActionListener, FocusListener {
050
051    private static final int RETRY_COUNT = 2;
052
053    public SplitVariableValue(String name, String comment, String cvName,
054            boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly,
055            String cvNum, String mask, int minVal, int maxVal,
056            HashMap<String, CvValue> v, JLabel status, String stdname,
057            String pSecondCV, int pFactor, int pOffset, String uppermask, String extra1, String extra2, String extra3, String extra4) {
058        super(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, v, status, stdname);
059        _minVal = 0;
060        _maxVal = ~0;
061        stepOneActions(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, minVal, maxVal, v, status, stdname, pSecondCV, pFactor, pOffset, uppermask, extra1, extra2, extra3, extra4);
062        _name = name;
063        _mask = mask; // will be converted to MaskArray to apply separate mask for each CV
064        if (mask != null && mask.contains(" ")) {
065            _maskArray = mask.split(" "); // type accepts multiple masks for SplitVariableValue
066        } else {
067            _maskArray = new String[1];
068            _maskArray[0] = mask;
069        }
070        _cvNum = cvNum;
071        _textField = new JTextField("0");
072        _defaultColor = _textField.getBackground();
073
074        _textField.setBackground(ValueState.UNKNOWN.getColor());
075        _textField.getAccessibleContext().setAccessibleName(label());
076
077        mFactor = pFactor;
078        mOffset = pOffset;
079        // legacy format variables
080        mSecondCV = pSecondCV;
081        _uppermask = uppermask;
082
083        // connect to the JTextField value
084        _textField.addActionListener(this);
085        _textField.addFocusListener(this);
086
087        log.debug("Variable={};comment={};cvName={};cvNum={};stdname={}", _name, comment, cvName, _cvNum, stdname);
088
089        // upper bit offset includes lower bit offset, and MSB bits missing from upper part
090        log.debug("Variable={}; upper mask {} had offsetVal={} so upperbitoffset={}", _name, _uppermask, offsetVal(_uppermask), offsetVal(_uppermask));
091
092        // set up array of used CVs
093        cvList = new ArrayList<>();
094
095        List<String> nameList = CvUtil.expandCvList(_cvNum); // see if cvName needs expanding
096        if (nameList.isEmpty()) {
097            // primary CV
098            String tMask;
099            if (_maskArray != null && _maskArray.length == 1) {
100                log.debug("PrimaryCV mask={}", _maskArray[0]);
101                tMask = _maskArray[0];
102            } else {
103                tMask = _mask; // mask supplied could be an empty string
104            }
105            cvList.add(new CvItem(_cvNum, tMask));
106
107            if (pSecondCV != null && !pSecondCV.equals("")) {
108                cvList.add(new CvItem(pSecondCV, _uppermask));
109            }
110        } else {
111            for (int i = 0; i < nameList.size(); i++) {
112                cvList.add(new CvItem(nameList.get(i), _maskArray[Math.min(i, _maskArray.length - 1)]));
113                // use last mask for all following CVs if fewer masks than the number of CVs listed were provided
114                log.debug("Added mask #{}: {}", i, _maskArray[Math.min(i, _maskArray.length - 1)]);
115            }
116        }
117
118        cvCount = cvList.size();
119
120        for (int i = 0; i < cvCount; i++) {
121            cvList.get(i).startOffset = currentOffset;
122            String t = cvList.get(i).cvMask;
123            if (t.contains("V")) {
124                currentOffset = currentOffset + t.lastIndexOf("V") - t.indexOf("V") + 1;
125            } else {
126                log.error("Variable={};cvName={};cvMask={} is an invalid bitmask", _name, cvList.get(i).cvName, cvList.get(i).cvMask);
127            }
128            log.debug("Variable={};cvName={};cvMask={};startOffset={};currentOffset={}", _name, cvList.get(i).cvName, cvList.get(i).cvMask, cvList.get(i).startOffset, currentOffset);
129
130            // connect CV for notification
131            CvValue cv = _cvMap.get(cvList.get(i).cvName);
132            cvList.get(i).thisCV = cv;
133        }
134
135        stepTwoActions();
136
137        _textField.setColumns(_columns);
138
139        // have to do when list is complete
140        for (int i = 0; i < cvCount; i++) {
141            var thisCV = cvList.get(i).thisCV;
142
143            // only add property listener once
144            boolean alreadyDone = false;
145            for (int j = 0; j < i; j++) {
146                if (thisCV.equals(cvList.get(j).thisCV) ) {
147                    alreadyDone = true;
148                    break;
149                }
150            }
151            if (! alreadyDone) {
152                thisCV.addPropertyChangeListener(this);
153            }
154            thisCV.setState(ValueState.FROMFILE);
155        }
156    }
157
158    /**
159     * Subclasses can override this to pick up constructor-specific attributes
160     * and perform other actions before cvList has been built.
161     *
162     * @param name      name.
163     * @param comment   comment.
164     * @param cvName    cv name.
165     * @param readOnly  true for read only, else false.
166     * @param infoOnly  true for info only, else false.
167     * @param writeOnly true for write only, else false.
168     * @param opsOnly   true for ops only, else false.
169     * @param cvNum     cv number.
170     * @param mask      cv mask.
171     * @param minVal    minimum value.
172     * @param maxVal    maximum value.
173     * @param v         hashmap of string and cv value.
174     * @param status    status.
175     * @param stdname   std name.
176     * @param pSecondCV second cv (no longer preferred, specify in cv)
177     * @param pFactor   factor.
178     * @param pOffset   offset.
179     * @param uppermask upper mask (no longer preferred, specify in mask)
180     * @param extra1    extra 1.
181     * @param extra2    extra 2.
182     * @param extra3    extra 3.
183     * @param extra4    extra 4.
184     */
185    public void stepOneActions(String name, String comment, String cvName,
186            boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly,
187            String cvNum, String mask, int minVal, int maxVal,
188            HashMap<String, CvValue> v, JLabel status, String stdname,
189            String pSecondCV, int pFactor, int pOffset, String uppermask, String extra1, String extra2, String extra3, String extra4) {
190        if (extra3 != null) {
191            _minVal = getValueFromText(extra3);
192        }
193        if (extra4 != null) {
194            _maxVal = getValueFromText(extra4);
195        }
196    }
197
198    /**
199     * Subclasses can override this to invoke further actions after cvList has
200     * been built.
201     */
202    public void stepTwoActions() {
203        if (currentOffset > bitCount) {
204            String eol = System.getProperty("line.separator");
205            throw new Error(
206                    "Decoder File parsing error:"
207                    + eol + "The Decoder Definition File specified \"" + _cvNum
208                    + "\" for variable \"" + _name + "\". This expands to:"
209                    + eol + "\"" + getCvDescription() + "\""
210                    + eol + "This requires " + currentOffset + " bits, which exceeds the " + bitCount
211                    + " bit capacity of the long integer used to store the variable."
212                    + eol + "The Decoder Definition File needs correction.");
213        }
214        _columns = cvCount * 2; //update column width now we have a better idea
215    }
216
217    @Override
218    public CvValue[] usesCVs() {
219        CvValue[] theseCvs = new CvValue[cvCount];
220        for (int i = 0; i < cvCount; i++) {
221            theseCvs[i] = cvList.get(i).thisCV;
222        }
223        return theseCvs;
224    }
225
226    /**
227     * Multiple masks can be defined for the CVs accessed by this variable.
228     * <br>
229     * Actual individual masks are returned in
230     * {@link #getCvDescription getCvDescription()}.
231     *
232     * @return The legacy two-CV mask if {@code highCV} is specified.
233     * <br>
234     * The {@code mask} if {@code highCV} is not specified.
235     */
236    @Override
237    public String getMask() {
238        if (mSecondCV != null && !mSecondCV.equals("")) {
239            return _uppermask + _mask;
240        } else {
241            return _mask; // a list of 1-n masks, separated by spaces
242        }
243    }
244
245    /**
246     * Access a specific mask, used in tests
247     *
248     * @param i index of CV in variable
249     * @return a single mask as string in the form XXXXVVVV, or empty string if
250     *         index out of bounds
251     */
252    protected String getMask(int i) {
253        if (i < cvCount) {
254            return cvList.get(i).cvMask;
255        }
256        return "";
257    }
258
259    /**
260     * Provide a user-readable description of the CVs accessed by this variable.
261     * <br>
262     * Actual individual masks are added to CVs if more are present.
263     *
264     * @return A user-friendly CV(s) and bitmask(s) description
265     */
266    @Override
267    public String getCvDescription() {
268        StringBuilder buf = new StringBuilder();
269        for (int i = 0; i < cvCount; i++) {
270            if (buf.length() > 0) {
271                buf.append(" & ");
272            }
273            buf.append("CV");
274            buf.append(cvList.get(i).cvName);
275            String temp = CvUtil.getMaskDescription(cvList.get(i).cvMask);
276            if (temp.length() > 0) {
277                buf.append(" ");
278                buf.append(temp);
279            }
280        }
281        buf.append("."); // mark that mask descriptions are already inserted for CvUtil.addCvDescription
282        return buf.toString();
283    }
284
285    String mSecondCV;
286    String _uppermask;
287    int mFactor;
288    int mOffset;
289    String _name;
290    String _mask; // full string as provided, use _maskArray to access one of multiple masks
291    String[] _maskArray;
292    String _cvNum;
293
294    List<CvItem> cvList;
295
296    int cvCount;
297    int currentOffset = 0;
298
299    @Override
300    public String getCvNum() {
301        String retString = "";
302        if (cvCount > 0) {
303            retString = cvList.get(0).cvName;
304        }
305        return retString;
306    }
307
308   @Override
309    public void setToolTipText(String t) {
310        super.setToolTipText(t);   // do default stuff
311        _textField.setToolTipText(t);  // set our value
312    }
313
314    // the connection is to cvNum and cvNum+1
315    long _minVal;
316    long _maxVal;
317
318    @Override
319    public Object rangeVal() {
320        return "Split value";
321    }
322
323    String oldContents = "0";
324
325    long getValueFromText(String s) {
326        return (Long.parseUnsignedLong(s));
327    }
328
329    String getTextFromValue(long v) {
330        return (Long.toUnsignedString(v));
331    }
332
333    int[] getCvValsFromTextField() {
334        long newEntry;  // entered value
335        try {
336            newEntry = getValueFromText(_textField.getText());
337        } catch (java.lang.NumberFormatException ex) {
338            newEntry = 0;
339        }
340
341        // calculate resulting number
342        long newVal = newEntry - mOffset;
343        // long newVal = Math.max(newEntry - mOffset, 0); // prevent negative values, especially in tests outside UI
344        if (mFactor != 0) {
345            newVal = newVal / mFactor;
346        } else {
347            log.error("Variable param 'factor' = 0 not valid; Decoder definition needs correction");
348        }
349        log.debug("Variable={};newEntry={};newVal={} with Offset={} + Factor={} applied", _name, newEntry, newVal, mOffset, mFactor);
350
351        int[] retVals = new int[cvCount];
352
353        // extract individual values via masks
354        for (int i = 0; i < cvCount; i++) {
355            retVals[i] = (((int) (newVal >>> cvList.get(i).startOffset))
356                    & (maskValAsInt(cvList.get(i).cvMask) >>> offsetVal(cvList.get(i).cvMask)));
357        }
358        return retVals;
359    }
360
361    /**
362     * Contains numeric-value specific code.
363     * <br><br>
364     * Calculates new value for _textField and invokes
365     * {@link #setLongValue(long) setLongValue(newVal)} to make and notify the
366     * change
367     *
368     * @param intVals array of new CV values
369     */
370    void updateVariableValue(int[] intVals) {
371
372        long newVal = 0;
373        for (int i = 0; i < intVals.length; i++) {
374            newVal = newVal | (((long) intVals[i]) << cvList.get(i).startOffset);
375            log.debug("Variable={}; i={}; newVal={}", _name, i, getTextFromValue(newVal));
376        }
377        log.debug("Variable={}; set value to {}", _name, newVal);
378        setLongValue(newVal);  // check for duplicate is done inside setLongValue
379        log.debug("Variable={}; in property change after setValue call", _name);
380    }
381
382    /**
383     * Saves contents of _textField to oldContents.
384     */
385    void enterField() {
386        oldContents = _textField.getText();
387    }
388
389    /**
390     * Contains numeric-value specific code.
391     * <br>
392     * firePropertyChange for "Value" with new and old contents of _textField
393     */
394    void exitField() {
395        // there may be a lost focus event left in the queue when disposed so protect
396        if (_textField != null && !oldContents.equals(_textField.getText())) {
397            long newFieldVal = 0;
398            try {
399                newFieldVal = getValueFromText(_textField.getText());
400            } catch (NumberFormatException e) {
401                _textField.setText(oldContents);
402            }
403            log.debug("_minVal={};_maxVal={};newFieldVal={}",
404                    Long.toUnsignedString(_minVal), Long.toUnsignedString(_maxVal), Long.toUnsignedString(newFieldVal));
405            if (Long.compareUnsigned(newFieldVal, _minVal) < 0 || Long.compareUnsigned(newFieldVal, _maxVal) > 0) {
406                _textField.setText(oldContents);
407            } else {
408                long newVal = (newFieldVal - mOffset) / mFactor;
409                long oldVal = (getValueFromText(oldContents) - mOffset) / mFactor;
410                log.debug("Enter updatedTextField from exitField");
411                updatedTextField();
412                prop.firePropertyChange("Value", oldVal, newVal);
413            }
414        }
415    }
416
417    boolean _fieldShrink = false;
418
419    @Override
420    void updatedTextField() {
421        log.debug("Variable='{}'; enter updatedTextField in {} with TextField='{}'", _name, (this.getClass().getSimpleName()), _textField.getText());
422        // called for new values in text field - set the CVs as needed
423
424        int[] retVals = getCvValsFromTextField();
425
426        // combine with existing values via mask
427        for (int j = 0; j < cvCount; j++) {
428            int i = j;
429            // special care needed if _textField is shrinking
430            if (_fieldShrink) {
431                i = (cvCount - 1) - j; // reverse CV updating order
432            }
433            log.debug("retVals[{}]={};cvList.get({}).cvMask{};offsetVal={}", i, retVals[i], i, cvList.get(i).cvMask, offsetVal(cvList.get(i).cvMask));
434            int cvMask = maskValAsInt(cvList.get(i).cvMask);
435            CvValue thisCV = cvList.get(i).thisCV;
436            int oldCvVal = thisCV.getValue();
437            int newCvVal = (oldCvVal & ~cvMask)
438                    | ((retVals[i] << offsetVal(cvList.get(i).cvMask)) & cvMask);
439            log.debug("{};cvMask={};oldCvVal={};retVals[{}]={};newCvVal={}", cvList.get(i).cvName, cvMask, oldCvVal, i, retVals[i], newCvVal);
440
441            // cv updates here trigger updated property changes, which means
442            // we're going to get notified sooner or later.
443            if (newCvVal != oldCvVal) {
444                thisCV.setValue(newCvVal);
445            }
446        }
447        log.debug("Variable={}; exit updatedTextField", _name);
448    }
449
450    /**
451     * ActionListener implementation.
452     * <p>
453     * Invokes {@link #exitField exitField()}
454     *
455     * @param e the action event
456     */
457    @Override
458    public void actionPerformed(ActionEvent e) {
459        log.debug("Variable='{}'; actionPerformed", _name);
460        exitField();
461    }
462
463    /**
464     * FocusListener implementations.
465     */
466    @Override
467    public void focusGained(FocusEvent e) {
468        log.debug("Variable={}; focusGained", _name);
469        enterField();
470    }
471
472    @Override
473    public void focusLost(FocusEvent e) {
474        log.debug("Variable={}; focusLost", _name);
475        exitField();
476    }
477
478    // to complete this class, fill in the routines to handle "Value" parameter
479    // and to read/write/hear parameter changes.
480    @Override
481    public String getValueString() {
482        log.debug("getValueString {}", _textField.getText());
483        return _textField.getText();
484    }
485
486    /**
487     * Set value from a String value.
488     *
489     * @param value a string representing the Long value to be set
490     */
491    @Override
492    public void setValue(String value) {
493        try {
494            long val = Long.parseUnsignedLong(value);
495            setLongValue(val);
496        } catch (NumberFormatException e) {
497            log.warn("skipping set of non-long value \"{}\"", value);
498        }
499    }
500
501    @Override
502    public void setIntValue(int i) {
503        setLongValue(i);
504    }
505
506    @Override
507    public int getIntValue() {
508        long x = getLongValue();
509        long y = x & intMask;
510        if ((Long.compareUnsigned(x, y) != 0)) {
511            log.error("Value {} from textField {} cannot be converted to 'int'", x, _name);
512        }
513        return (int) ((getValueFromText(_textField.getText()) - mOffset) / mFactor);
514    }
515
516    /**
517     * Get the value as an unsigned long.
518     *
519     * @return the value as a long
520     */
521    @Override
522    public long getLongValue() {
523        return ((getValueFromText(_textField.getText()) - mOffset) / mFactor);
524    }
525
526    @Override
527    public Object getValueObject() {
528        return getLongValue();
529    }
530
531    @Override
532    public Component getCommonRep() {
533        if (getReadOnly()) {
534            JLabel r = new JLabel(_textField.getText());
535            updateRepresentation(r);
536            return r;
537        } else {
538            return _textField;
539        }
540    }
541
542    public void setLongValue(long value) {
543        log.debug("Variable={}; enter setLongValue {}", _name, value);
544        long oldVal;
545        try {
546            oldVal = (getValueFromText(_textField.getText()) - mOffset) / mFactor;
547        } catch (java.lang.NumberFormatException ex) {
548            oldVal = -999;
549        }
550        log.debug("Variable={}; setValue with new value {} old value {}", _name, value, oldVal);
551        _textField.setText(getTextFromValue(value * mFactor + mOffset));
552        if (oldVal != value || getState() == ValueState.UNKNOWN) {
553            actionPerformed(null);
554        }
555        // TODO PENDING: the code used to fire value * mFactor + mOffset, which is a text representation;
556        // but 'oldValue' was converted back using mOffset / mFactor making those two (new / old)
557        // using different scales. Probably a bug, but it has been there from well before
558        // the extended splitVal. Because of the risk of breaking existing
559        // behaviour somewhere, deferring correction until at least the next test release.
560        prop.firePropertyChange("Value", oldVal, value * mFactor + mOffset);
561        log.debug("Variable={}; exit setLongValue old={} new={}", _name, oldVal, value);
562    }
563
564    Color _defaultColor;
565
566    // implement an abstract member to set colors
567    @Override
568    void setColor(Color c) {
569        if (c != null) {
570            _textField.setBackground(c);
571            log.debug("Variable={}; Set Color to {}", _name, c);
572        } else {
573            log.debug("Variable={}; Set Color to defaultColor {}", _name, _defaultColor.toString());
574            _textField.setBackground(_defaultColor);
575        }
576        // prop.firePropertyChange("Value", null, null);
577    }
578
579    int _columns = 1;
580
581    @Override
582    public Component getNewRep(String format) {
583        JTextField value = new VarTextField(_textField.getDocument(), _textField.getText(), _columns, this);
584        if (getReadOnly() || getInfoOnly()) {
585            value.setEditable(false);
586        }
587        reps.add(value);
588        return updateRepresentation(value);
589    }
590
591    @Override
592    public void setAvailable(boolean a) {
593        _textField.setVisible(a);
594        for (Component c : reps) {
595            c.setVisible(a);
596        }
597        super.setAvailable(a);
598    }
599
600    java.util.List<Component> reps = new java.util.ArrayList<>();
601
602    private int retry = 0;
603    private int _progState = 0;
604    private static final int IDLE = 0;
605    private static final int READING_FIRST = 1;
606    private static final int WRITING_FIRST = -1;
607    private static final int bitCount = Long.bitCount(~0);
608    private static final long intMask = Integer.toUnsignedLong(~0);
609
610    /**
611     * Notify the connected CVs of a state change from above
612     *
613     * @param state The new state
614     */
615    @Override
616    public void setCvState(ValueState state) {
617        for (int i = 0; i < cvCount; i++) {
618            cvList.get(i).thisCV.setState(state);
619        }
620    }
621
622    @Override
623    public boolean isChanged() {
624        boolean changed = false;
625        for (int i = 0; i < cvCount; i++) {
626            changed = (changed || considerChanged(cvList.get(i).thisCV));
627        }
628        return changed;
629    }
630
631    @Override
632    public boolean isToRead() {
633        boolean toRead = false;
634        for (int i = 0; i < cvCount; i++) {
635            toRead = (toRead || (cvList.get(i).thisCV).isToRead());
636        }
637        return toRead;
638    }
639
640    @Override
641    public boolean isToWrite() {
642        boolean toWrite = false;
643        for (int i = 0; i < cvCount; i++) {
644            toWrite = (toWrite || (cvList.get(i).thisCV).isToWrite());
645        }
646        return toWrite;
647    }
648
649    @Override
650    public void readChanges() {
651        if (isToRead() && !isChanged()) {
652            log.debug("!!!!!!! unacceptable combination in readChanges: {}", label());
653        }
654        if (isChanged() || isToRead()) {
655            readAll();
656        }
657    }
658
659    @Override
660    public void writeChanges() {
661        if (isToWrite() && !isChanged()) {
662            log.debug("!!!!!! unacceptable combination in writeChanges: {}", label());
663        }
664        if (isChanged() || isToWrite()) {
665            writeAll();
666        }
667    }
668
669    @Override
670    public void readAll() {
671        log.debug("Variable={}; splitVal read() invoked", _name);
672        setToRead(false);
673        setBusy(true);  // will be reset when value changes
674        //super.setState(READ);
675        if (_progState != IDLE) {
676            log.warn("Variable={}; programming state {}, not IDLE, in read()", _name, _progState);
677        }
678        _textField.setText(""); // start with a clean slate
679        for (int i = 0; i < cvCount; i++) { // mark all Cvs as unknown otherwise problems occur
680            cvList.get(i).thisCV.setState(ValueState.UNKNOWN);
681        }
682        _progState = READING_FIRST;
683        retry = 0;
684        log.debug("Variable={}; Start CV read", _name);
685        log.debug("Reading CV={}", cvList.get(0).cvName);
686        (cvList.get(0).thisCV).read(_status); // kick off the read sequence
687    }
688
689    @Override
690    public void writeAll() {
691        log.debug("Variable={}; write() invoked", _name);
692        if (getReadOnly()) {
693            log.error("Variable={}; unexpected write operation when readOnly is set", _name);
694        }
695        setToWrite(false);
696        setBusy(true);  // will be reset when value changes
697        if (_progState != IDLE) {
698            log.warn("Variable={}; Programming state {}, not IDLE, in write()", _name, _progState);
699        }
700        _progState = WRITING_FIRST;
701        log.debug("Variable={}; Start CV write", _name);
702        log.debug("Writing CV={}", cvList.get(0).cvName);
703        (cvList.get(0).thisCV).write(_status); // kick off the write-sequence
704    }
705
706    /**
707     * Assigns a priority value to a given state.
708     *
709     * @param state State to be converted to a priority value
710     * @return Priority value from state, with UNKNOWN numerically highest
711     */
712    @SuppressFBWarnings(value = {"SF_SWITCH_NO_DEFAULT", "SF_SWITCH_FALLTHROUGH"}, justification = "Intentional fallthrough to produce correct value")
713    int priorityValue(ValueState state) {
714        int value = 0;
715        switch (state) {
716            case UNKNOWN:
717                value++;
718            //$FALL-THROUGH$
719            case DIFFERENT:
720                value++;
721            //$FALL-THROUGH$
722            case EDITED:
723                value++;
724            //$FALL-THROUGH$
725            case FROMFILE:
726                value++;
727            //$FALL-THROUGH$
728            default:
729                //$FALL-THROUGH$
730                return value;
731        }
732    }
733
734    // handle incoming parameter notification
735    @Override
736    public void propertyChange(java.beans.PropertyChangeEvent e) {
737        log.debug("Variable={} source={}; property {} changed from {} to {}", _name, e.getSource().toString(), e.getPropertyName(), e.getOldValue(),e.getNewValue());
738        // notification from CV; check for Value being changed
739        if (e.getPropertyName().equals("Busy") && e.getNewValue().equals(Boolean.FALSE)) {
740            log.debug("*** Busy -> false transition from {}", e.getSource());
741            // busy transitions drive the state
742            if (_progState != IDLE) {
743                log.debug("Variable={} source={}; getState() = {}", _name, e.getSource().toString(), (cvList.get(Math.abs(_progState) - 1).thisCV).getState());
744            }
745
746            if (_progState == IDLE) { // State machine is idle, so "Busy" transition is the result of a CV update by another source.
747                // The source would be a Read/Write from either the CVs pane or another Variable with one or more overlapping CV(s).
748                // It is definitely not an error condition, but needs to be ignored by this variable's state machine.
749                log.debug("Variable={}; Busy goes false with _progState IDLE, so ignore by state machine", _name);
750            } else if (_progState >= READING_FIRST) {   // reading CVs
751                if ((cvList.get(Math.abs(_progState) - 1).thisCV).getState() == ValueState.READ) {   // was the last read successful?
752                    retry = 0;
753                    if (Math.abs(_progState) < cvCount) {   // read next CV
754                        _progState++;
755                        log.debug("Reading CV={} with state {}", cvList.get(Math.abs(_progState) - 1).cvName, _progState);
756                        (cvList.get(Math.abs(_progState) - 1).thisCV).read(_status);
757                    } else {  // finally done, set not busy
758                        log.debug("Variable={}; Busy goes false with success READING _progState {}", _name, _progState);
759                        _progState = IDLE;
760                        setBusy(false);
761                    }
762                } else {   // read failed
763                    log.debug("Variable={}; Busy goes false with failure READING _progState {}", _name, _progState);
764                    if (retry < RETRY_COUNT) { //have we exhausted retry count?
765                        retry++;
766                        (cvList.get(Math.abs(_progState) - 1).thisCV).read(_status);
767                    } else {
768                        _progState = IDLE;
769                        setBusy(false);
770                        if (RETRY_COUNT > 0) {
771                            for (int i = 0; i < cvCount; i++) { // mark all CVs as unknown otherwise problems may occur
772                                cvList.get(i).thisCV.setState(ValueState.UNKNOWN);
773                            }
774                        }
775                    }
776                }
777            } else {  // writing CVs
778                if ((cvList.get(Math.abs(_progState) - 1).thisCV).getState() == ValueState.STORED) {   // was the last read successful?
779                    if (Math.abs(_progState) < cvCount) {   // write next CV
780                        _progState--;
781                        log.debug("Writing CV={}", cvList.get(Math.abs(_progState) - 1).cvName);
782                        (cvList.get(Math.abs(_progState) - 1).thisCV).write(_status);
783                    } else {  // finally done, set not busy
784                        log.debug("Variable={}; Busy goes false with success WRITING _progState {}", _name, _progState);
785                        _progState = IDLE;
786                        setBusy(false);
787                    }
788                } else {   // read failed we're done!
789                    log.debug("Variable={}; Busy goes false with failure WRITING _progState {}", _name, _progState);
790                    _progState = IDLE;
791                    setBusy(false);
792                }
793            }
794        } else if (e.getPropertyName().equals("State")) {
795            log.debug("Possible {} variable state change due to CV state change, so propagate that", _name);
796            ValueState varState = getState(); // AbstractValue.SAME;
797            log.debug("{} variable state was {}", _name, varState.getName());
798            for (int i = 0; i < cvCount; i++) {
799                var state = cvList.get(i).thisCV.getState();
800                if (i == 0) {
801                    varState = state;
802                } else if (priorityValue(state) > priorityValue(varState)) {
803                    //varState = AbstractValue.UNKNOWN; // or should it be = state ?
804                    varState = state; // or should it be = state ?
805                }
806            }
807            setState(varState);
808            log.debug("{} variable state set to {}", _name, varState.getName());
809        } else if (e.getPropertyName().equals("Value")) {
810            // update value of Variable
811            log.debug("update value of Variable {}", _name);
812
813            int[] intVals = new int[cvCount];
814
815            for (int i = 0; i < cvCount; i++) {
816                intVals[i] = (cvList.get(i).thisCV.getValue() & maskValAsInt(cvList.get(i).cvMask)) >>> offsetVal(cvList.get(i).cvMask);
817            }
818
819            updateVariableValue(intVals);
820
821            log.debug("state change due to CV value change, so propagate that");
822            ValueState varState = ValueState.SAME;
823            for (int i = 0; i < cvCount; i++) {
824                ValueState state = cvList.get(i).thisCV.getState();
825                if (priorityValue(state) > priorityValue(varState)) {
826                    varState = state;
827                }
828            }
829            setState(varState);
830        }
831    }
832
833    // stored reference to the JTextField
834    JTextField _textField;
835
836    /* Internal class extends a JTextField so that its color is consistent with
837     * an underlying variable
838     *
839     * @author Bob Jacobsen   Copyright (C) 2001
840     *
841     */
842    public class VarTextField extends JTextField {
843
844        VarTextField(Document doc, String text, int col, SplitVariableValue var) {
845            super(doc, text, col);
846            _var = var;
847            // get the original color right
848            setBackground(_var._textField.getBackground());
849            // listen for changes to ourself
850            addActionListener(this::thisActionPerformed);
851            addFocusListener(new java.awt.event.FocusListener() {
852                @Override
853                public void focusGained(FocusEvent e) {
854                    log.debug("Variable={}; focusGained", _name);
855                    enterField();
856                }
857
858                @Override
859                public void focusLost(FocusEvent e) {
860                    log.debug("Variable={}; focusLost", _name);
861                    exitField();
862                }
863            });
864            // listen for changes to original state
865            _var.addPropertyChangeListener(this::originalPropertyChanged);
866        }
867
868        SplitVariableValue _var;
869
870        void thisActionPerformed(java.awt.event.ActionEvent e) {
871            // tell original
872            _var.actionPerformed(e);
873        }
874
875        void originalPropertyChanged(java.beans.PropertyChangeEvent e) {
876            // update this color from original state
877            if (e.getPropertyName().equals("State")) {
878                setBackground(_var._textField.getBackground());
879            }
880        }
881
882    }
883
884    /**
885     * Class to hold CV parameters for CVs used.
886     */
887    static class CvItem {
888
889        // class fields
890        String cvName;
891        String cvMask;
892        int startOffset;
893        CvValue thisCV;
894
895        CvItem(String cvNameVal, String cvMaskVal) {
896            cvName = cvNameVal;
897            cvMask = cvMaskVal;
898        }
899    }
900
901    // clean up connections when done
902    @Override
903    public void dispose() {
904        log.debug("dispose");
905        if (_textField != null) {
906            _textField.removeActionListener(this);
907        }
908        for (int i = 0; i < cvCount; i++) {
909            (_cvMap.get(cvList.get(i).cvName)).removePropertyChangeListener(this);
910        }
911
912        _textField = null;
913        _maskArray = null;
914        // do something about the VarTextField
915    }
916
917    // initialize logging
918    private static final Logger log = LoggerFactory.getLogger(SplitVariableValue.class);
919
920}