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