001package jmri.jmrit.symbolicprog;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.awt.Color;
006import java.awt.Component;
007import java.awt.event.ActionEvent;
008import java.awt.event.ActionListener;
009import java.awt.event.FocusEvent;
010import java.awt.event.FocusListener;
011import java.util.*;
012
013import javax.swing.tree.DefaultMutableTreeNode;
014import javax.swing.tree.TreePath;
015
016import javax.swing.ComboBoxModel;
017import javax.swing.JComboBox;
018import javax.swing.JLabel;
019import javax.swing.JScrollPane;
020import javax.swing.JTree;
021import javax.swing.event.TreeSelectionEvent;
022import javax.swing.event.TreeSelectionListener;
023import javax.swing.tree.DefaultTreeModel;
024import javax.swing.tree.DefaultTreeSelectionModel;
025
026import jmri.util.CvUtil;
027
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030
031/**
032 * Extends VariableValue to represent a variable split across multiple CVs with
033 * values from a pre-selected range each of which is associated with a text name
034 * (aka, a drop down)
035 * <br>
036 * The {@code mask} attribute represents the part of the value that's present in
037 * each CV; higher-order bits are loaded to subsequent CVs.<br>
038 * It is possible to assign a specific mask for each CV by providing a space
039 * separated list of masks, starting with the lowest, and matching the order of
040 * CVs
041 * <br><br>
042 * The original use was for addresses of stationary (accessory) decoders.
043 * <br>
044 * The original version only allowed two CVs, with the second CV specified by
045 * the attributes {@code highCV} and {@code upperMask}.
046 * <br><br>
047 * The preferred technique is now to specify all CVs in the {@code CV} attribute
048 * alone, as documented at {@link CvUtil#expandCvList expandCvList(String)}.
049 * <br><br>
050 * Optional attributes {@code factor} and {@code offset} are applied when going
051 * <i>from</i> the variable value <i>to</i> the CV values, or vice-versa:
052 * <pre>
053 * Value to put in CVs = ((value in text field) -{@code offset})/{@code factor}
054 * Value to put in text field = ((value in CVs) *{@code factor}) +{@code offset}
055 * </pre>
056 *
057 * @author Bob Jacobsen Copyright (C) 2002, 2003, 2004, 2013
058 * @author Dave Heap Copyright (C) 2016, 2019
059 * @author Egbert Broerse Copyright (C) 2020
060 * @author Jordan McBride Copyright (C) 2021
061 */
062public class SplitEnumVariableValue extends VariableValue
063        implements ActionListener, FocusListener {
064
065    private static final int RETRY_COUNT = 2;
066
067    int atest = 1;
068    private final List<JTree> trees = new ArrayList<>();
069
070    private final List<ComboCheckBox> comboCBs = new ArrayList<>();
071    private final List<SplitEnumVariableValue.VarComboBox> comboVars = new ArrayList<>();
072    private final List<ComboRadioButtons> comboRBs = new ArrayList<>();
073
074
075    public SplitEnumVariableValue(String name, String comment, String cvName,
076            boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly,
077            String cvNum, String mask, int minVal, int maxVal,
078            HashMap<String, CvValue> v, JLabel status, String stdname,
079            String pSecondCV, int pFactor, int pOffset, String uppermask, String extra1, String extra2, String extra3, String extra4) {
080        super(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, v, status, stdname);
081        _minVal = 0;
082        _maxVal = ~0;
083        stepOneActions(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, minVal, maxVal, v, status, stdname, pSecondCV, pFactor, pOffset, uppermask, extra1, extra2, extra3, extra4);
084        _name = name;
085        _mask = mask; // will be converted to MaskArray to apply separate mask for each CV
086        if (mask != null && mask.contains(" ")) {
087            _maskArray = mask.split(" "); // type accepts multiple masks for SplitVariableValue
088        } else {
089            _maskArray = new String[1];
090            _maskArray[0] = mask;
091        }
092        _cvNum = cvNum;
093        mFactor = pFactor;
094        mOffset = pOffset;
095        // legacy format variables
096        mSecondCV = pSecondCV;
097        _uppermask = uppermask;
098
099
100        log.debug("Variable={};comment={};cvName={};cvNum={};stdname={}", _name, comment, cvName, _cvNum, stdname);
101
102        // upper bit offset includes lower bit offset, and MSB bits missing from upper part
103        log.debug("Variable={}; upper mask {} had offsetVal={} so upperbitoffset={}", _name, _uppermask, offsetVal(_uppermask), offsetVal(_uppermask));
104
105        // set up array of used CVs
106        cvList = new ArrayList<>();
107
108        List<String> nameList = CvUtil.expandCvList(_cvNum); // see if cvName needs expanding
109        if (nameList.isEmpty()) {
110            // primary CV
111            String tMask;
112            if (_maskArray != null && _maskArray.length == 1) {
113                log.debug("PrimaryCV mask={}", _maskArray[0]);
114                tMask = _maskArray[0];
115            } else {
116                tMask = _mask; // mask supplied could be an empty string
117            }
118            cvList.add(new CvItem(_cvNum, tMask));
119
120            if (pSecondCV != null && !pSecondCV.equals("")) {
121                cvList.add(new CvItem(pSecondCV, _uppermask));
122            }
123        } else {
124            for (int i = 0; i < nameList.size(); i++) {
125                cvList.add(new CvItem(nameList.get(i), _maskArray[Math.min(i, _maskArray.length - 1)]));
126                // use last mask for all following CVs if fewer masks than the number of CVs listed were provided
127                log.debug("Added mask #{}: {}", i, _maskArray[Math.min(i, _maskArray.length - 1)]);
128            }
129        }
130
131        cvCount = cvList.size();
132
133        for (int i = 0; i < cvCount; i++) {
134            cvList.get(i).startOffset = currentOffset;
135            String t = cvList.get(i).cvMask;
136            if (t.contains("V")) {
137                currentOffset = currentOffset + t.lastIndexOf("V") - t.indexOf("V") + 1;
138            } else {
139                log.error("Variable={};cvName={};cvMask={} is an invalid bitmask", _name, cvList.get(i).cvName, cvList.get(i).cvMask);
140            }
141            log.debug("Variable={};cvName={};cvMask={};startOffset={};currentOffset={}", _name, cvList.get(i).cvName, cvList.get(i).cvMask, cvList.get(i).startOffset, currentOffset);
142
143            // connect CV for notification
144            CvValue cv = _cvMap.get(cvList.get(i).cvName);
145            cvList.get(i).thisCV = cv;
146        }
147
148        stepTwoActions();
149
150
151        // have to do when list is complete
152        for (int i = 0; i < cvCount; i++) {
153            var thisCV = cvList.get(i).thisCV;
154
155            // only add property listener once
156            boolean alreadyDone = false;
157            for (int j = 0; j < i; j++) {
158                if (thisCV.equals(cvList.get(j).thisCV) ) {
159                    alreadyDone = true;
160                    break;
161                }
162            }
163            if (! alreadyDone) {
164                thisCV.addPropertyChangeListener(this);
165            }
166            thisCV.setState(ValueState.FROMFILE);
167        }
168
169        treeNodes.addLast(new DefaultMutableTreeNode(""));
170    }
171
172    /**
173     * Subclasses can override this to pick up constructor-specific attributes
174     * and perform other actions before cvList has been built.
175     *
176     * @param name      name.
177     * @param comment   comment.
178     * @param cvName    cv name.
179     * @param readOnly  true for read only, else false.
180     * @param infoOnly  true for info only, else false.
181     * @param writeOnly true for write only, else false.
182     * @param opsOnly   true for ops only, else false.
183     * @param cvNum     cv number.
184     * @param mask      cv mask.
185     * @param minVal    minimum value.
186     * @param maxVal    maximum value.
187     * @param v         hashmap of string and cv value.
188     * @param status    status.
189     * @param stdname   std name.
190     * @param pSecondCV second cv (no longer preferred, specify in cv)
191     * @param pFactor   factor.
192     * @param pOffset   offset.
193     * @param uppermask upper mask (no longer preferred, specify in mask)
194     * @param extra1    extra 1.
195     * @param extra2    extra 2.
196     * @param extra3    extra 3.
197     * @param extra4    extra 4.
198     */
199    public void stepOneActions(String name, String comment, String cvName,
200            boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly,
201            String cvNum, String mask, int minVal, int maxVal,
202            HashMap<String, CvValue> v, JLabel status, String stdname,
203            String pSecondCV, int pFactor, int pOffset, String uppermask, String extra1, String extra2, String extra3, String extra4) {
204        if (extra3 != null) {
205            _minVal = getValueFromText(extra3);
206        }
207        if (extra4 != null) {
208            _maxVal = getValueFromText(extra4);
209        }
210    }
211
212    public void nItems(int n) {
213        _itemArray = new String[n];
214        _pathArray = new TreePath[n];
215        _valueArray = new int[n];
216        _nstored = 0;
217        log.debug("enumeration arrays size={}", n);
218    }
219
220        /**
221     * Create a new item in the enumeration, with an associated value one more
222     * than the last item (or zero if this is the first one added)
223     *
224     * @param s Name of the enumeration item
225     */
226    public void addItem(String s) {
227        if (_nstored == 0) {
228            addItem(s, 0);
229        } else {
230            addItem(s, _valueArray[_nstored - 1] + 1);
231        }
232    }
233
234    public void addItem(String s, int value) {
235        _valueArray[_nstored] = value;
236        SplitEnumVariableValue.TreeLeafNode node = new SplitEnumVariableValue.TreeLeafNode(s, _nstored);
237        treeNodes.getLast().add(node);
238        _pathArray[_nstored] = new TreePath(node.getPath());
239        _itemArray[_nstored++] = s;
240        log.debug("_itemArray.length={},_nstored={},s='{}',value={}", _itemArray.length, _nstored, s, value);
241    }
242
243    public void startGroup(String name) {
244        DefaultMutableTreeNode next = new DefaultMutableTreeNode(name);
245        treeNodes.getLast().add(next);
246        treeNodes.addLast(next);
247    }
248
249    public void endGroup() {
250        treeNodes.removeLast();
251    }
252
253    public void lastItem() {
254        _value = new JComboBox<>(java.util.Arrays.copyOf(_itemArray, _nstored));
255        _value.getAccessibleContext().setAccessibleName(label());
256
257        // finish initialization
258        _value.setActionCommand("");
259        _defaultColor = _value.getBackground();
260        _value.setBackground(ValueState.UNKNOWN.getColor());
261        _value.setOpaque(true);
262        // connect to the JComboBox model and the CV so we'll see changes.
263        _value.addActionListener(this);
264        CvValue cv1 = cvList.get(0).thisCV;
265        CvValue cv2 = cvList.get(1).thisCV;
266        if (cv1 == null || cv2 == null) {
267            log.error("no CV defined in enumVal {}, skipping setState", getCvName());
268            return;
269        }
270        cv1.addPropertyChangeListener(this);
271        cv1.setState(ValueState.FROMFILE);
272        if (! cv1.equals(cv2)) {  // only add listener once
273            cv2.addPropertyChangeListener(this);
274        }
275        cv2.setState(ValueState.FROMFILE);
276    }
277
278
279
280    @Override
281    public void setToolTipText(String t) {
282        super.setToolTipText(t);   // do default stuff
283        _value.setToolTipText(t);  // set our value
284    }
285        // stored value
286    JComboBox<String> _value = null;
287
288    // place to keep the items & associated numbers
289    private String[] _itemArray = null;
290    private TreePath[] _pathArray = null;
291    private int[] _valueArray = null;
292    private int _nstored;
293
294    Deque<DefaultMutableTreeNode> treeNodes = new ArrayDeque<>();
295
296    /**
297     * Subclasses can override this to invoke further actions after cvList has
298     * been built.
299     */
300    public void stepTwoActions() {
301        if (currentOffset > bitCount) {
302            String eol = System.getProperty("line.separator");
303            throw new Error(
304                    "Decoder File parsing error:"
305                    + eol + "The Decoder Definition File specified \"" + _cvNum
306                    + "\" for variable \"" + _name + "\". This expands to:"
307                    + eol + "\"" + getCvDescription() + "\""
308                    + eol + "This requires " + currentOffset + " bits, which exceeds the " + bitCount
309                    + " bit capacity of the long integer used to store the variable."
310                    + eol + "The Decoder Definition File needs correction.");
311        }
312        _columns = cvCount * 2; //update column width now we have a better idea
313    }
314
315    @Override
316    public void setAvailable(boolean a) {
317        _value.setVisible(a);
318        for (ComboCheckBox c : comboCBs) {
319            c.setVisible(a);
320        }
321        for (SplitEnumVariableValue.VarComboBox c : comboVars) {
322            c.setVisible(a);
323        }
324        for (ComboRadioButtons c : comboRBs) {
325            c.setVisible(a);
326        }
327        super.setAvailable(a);
328    }
329
330    /**
331     * Simple request getter for the CVs composing this variable
332     * <br>
333     * @return Array of CvValue for all of associated CVs
334     */
335    @Override
336    public CvValue[] usesCVs() {
337        CvValue[] theseCvs = new CvValue[cvCount];
338        for (int i = 0; i < cvCount; i++) {
339            theseCvs[i] = cvList.get(i).thisCV;
340        }
341        return theseCvs;
342    }
343
344    /**
345     * Multiple masks can be defined for the CVs accessed by this variable.
346     * <br>
347     * Actual individual masks are returned in
348     * {@link #getCvDescription getCvDescription()}.
349     *
350     * @return The legacy two-CV mask if {@code highCV} is specified.
351     * <br>
352     * The {@code mask} if {@code highCV} is not specified.
353     */
354    @Override
355    public String getMask() {
356        if (mSecondCV != null && !mSecondCV.equals("")) {
357            return _uppermask + _mask;
358        } else {
359            return _mask; // a list of 1-n masks, separated by spaces
360        }
361    }
362
363    /**
364     * Access a specific mask, used in tests
365     *
366     * @param i index of CV in variable
367     * @return a single mask as string in the form XXXXVVVV, or empty string if
368     *         index out of bounds
369     */
370    protected String getMask(int i) {
371        if (i < cvCount) {
372            return cvList.get(i).cvMask;
373        }
374        return "";
375    }
376
377    /**
378     * Provide a user-readable description of the CVs accessed by this variable.
379     * <br>
380     * Actual individual masks are added to CVs if more are present.
381     *
382     * @return A user-friendly CV(s) and bitmask(s) description
383     */
384    @Override
385    public String getCvDescription() {
386        StringBuilder buf = new StringBuilder();
387        for (int i = 0; i < cvCount; i++) {
388            if (buf.length() > 0) {
389                buf.append(" & ");
390            }
391            buf.append("CV");
392            buf.append(cvList.get(i).cvName);
393            String temp = CvUtil.getMaskDescription(cvList.get(i).cvMask);
394            if (temp.length() > 0) {
395                buf.append(" ");
396                buf.append(temp);
397            }
398        }
399        buf.append("."); // mark that mask descriptions are already inserted for CvUtil.addCvDescription
400        return buf.toString();
401    }
402
403    String mSecondCV;
404    String _uppermask;
405    int mFactor;
406    int mOffset;
407    String _name;
408    String _mask; // full string as provided, use _maskArray to access one of multiple masks
409    String[] _maskArray = new String[0];
410    String _cvNum;
411
412    List<CvItem> cvList;
413
414    int cvCount = 0;
415    int currentOffset = 0;
416
417    /**
418     * Get the first CV from the set used to define this variable
419     * <br>
420     * @return The legacy two-CV mask if {@code highCV} is specified.
421     */
422    @Override
423    public String getCvNum() {
424        String retString = "";
425        if (cvCount > 0) {
426            retString = cvList.get(0).cvName;
427        }
428        return retString;
429    }
430
431    long _minVal;
432    long _maxVal;
433
434    @Override
435    public Object rangeVal() {
436        return "Split value";
437    }
438
439    String oldContents = "0";
440
441    long getValueFromText(String s) {
442        return (Long.parseUnsignedLong(s));
443    }
444
445    String getTextFromValue(long v) {
446        return (Long.toUnsignedString(v));
447    }
448
449    /**
450     * Contains numeric-value specific code.
451     * <br><br>
452     * Calculates new value for _enumField and invokes
453     * {@link #setLongValue(long) setLongValue(newVal)} to make and notify the
454     * change
455     *
456     * @param intVals array of new CV values
457     */
458    void updateVariableValue(int[] intVals) {
459        if (intVals.length > 0){
460            long newVal = 0;
461            for (int i = 0; i < intVals.length; i++) {
462                newVal = newVal | (((long) intVals[i]) << cvList.get(i).startOffset);
463                log.debug("Variable={}; i={}; intVals={}; startOffset={}; newVal={}",
464                    _name, i, intVals[i], cvList.get(i).startOffset, getTextFromValue(newVal));
465            }
466            log.debug("Variable={}; set value to {}", _name, newVal);
467            setLongValue(newVal);  // check for duplicate is done inside setLongValue
468            log.debug("Variable={}; in property change after setValue call", _name);
469        }
470    }
471
472    /**
473     * Saves selected item from _value (enumField) to oldContents.
474     */
475    void enterField() {
476        oldContents =  String.valueOf(_value.getSelectedItem());
477        log.debug("enterField sets oldContents to {}", oldContents);
478    }
479
480    /**
481     * Contains numeric-value specific code.
482     * <br>
483     * firePropertyChange for "Value" with new and old contents of _enumField
484     */
485    void exitField(){
486        // there may be a lost focus event left in the queue when disposed so protect
487        log.trace("exitField starts");
488        if (_value != null && !oldContents.equals(_value.getSelectedItem())) {
489            long newFieldVal = 0;
490            try {
491                newFieldVal = Long.parseLong((String) Objects.requireNonNull(_value.getSelectedItem()));
492            } catch (NumberFormatException e) {
493                //_value.setText(oldContents);
494            }
495            log.debug("_minVal={};_maxVal={};newFieldVal={}",
496                    Long.toUnsignedString(_minVal), Long.toUnsignedString(_maxVal), Long.toUnsignedString(newFieldVal));
497            if (Long.compareUnsigned(newFieldVal, _minVal) < 0 || Long.compareUnsigned(newFieldVal, _maxVal) > 0) {
498
499            } else {
500                long newVal = (newFieldVal - mOffset) / mFactor;
501                long oldVal = (getValueFromText(oldContents) - mOffset) / mFactor;
502                prop.firePropertyChange("Value", oldVal, newVal);
503            }
504        }
505        log.trace("exitField ends");
506    }
507
508    boolean _fieldShrink = false;
509
510    void updatedDropDown() {
511        log.debug("Variable='{}'; enter updatedDropDown in {} with DropDownValue='{}'", _name, (this.getClass().getSimpleName()), _value.getSelectedIndex());
512        // called for new values in text field - set the CVs as needed
513
514        int[] retVals = getCvValsFromSingleInt(getIntValue());
515
516        // combine with existing values via mask
517        for (int j = 0; j < cvCount; j++) {
518            int i = j;
519            log.debug("retVals[{}]={};cvList.get({}).cvMask{};offsetVal={}", i, retVals[i], i, cvList.get(i).cvMask, offsetVal(cvList.get(i).cvMask));
520            int cvMask = maskValAsInt(cvList.get(i).cvMask);
521            CvValue thisCV = cvList.get(i).thisCV;
522            int oldCvVal = thisCV.getValue();
523            int newCvVal = (oldCvVal & ~cvMask)
524                    | ((retVals[i] << offsetVal(cvList.get(i).cvMask)) & cvMask);
525            log.debug("{};cvMask={};oldCvVal={};retVals[{}]={};newCvVal={}", cvList.get(i).cvName, cvMask, oldCvVal, i, retVals[i], newCvVal);
526
527            // cv updates here trigger updated property changes, which means
528            // we're going to get notified sooner or later.
529            if (newCvVal != oldCvVal) {
530                thisCV.setValue(newCvVal);
531            }
532        }
533        log.debug("Variable={}; exit updatedDropDown", _name);
534    }
535
536    int[] getCvValsFromSingleInt(long newEntry) {
537        // calculate resulting number
538        long newVal = (newEntry - mOffset) / mFactor;
539        log.debug("getCvValsFromSingleInt Variable={};newEntry={};newVal={} with Offset={} + Factor={} applied", _name, newEntry, newVal, mOffset, mFactor);
540
541        int[] retVals = new int[cvCount];
542
543        // extract individual values via masks
544        for (int i = 0; i < cvCount; i++) {
545            log.trace("      Starting with newVal={} startOffset={} mask={} offsetVal={}",
546                        newVal, cvList.get(i).startOffset, maskValAsInt(cvList.get(i).cvMask), offsetVal(cvList.get(i).cvMask));
547            retVals[i] = (((int) (newVal >>> cvList.get(i).startOffset))
548                    & (maskValAsInt(cvList.get(i).cvMask) >>> offsetVal(cvList.get(i).cvMask)));
549            log.trace("      Calculated {} entry is {}", i, retVals[i]);
550        }
551        return retVals;
552    }
553
554    /**
555     * ActionListener implementation. Called by new selection in the JComboBox representation.
556     * <p>
557     * Invokes {@link #exitField exitField()}
558     *
559     * @param e the action event
560     */
561    @Override
562    public void actionPerformed(ActionEvent e) {
563        // see if this is from _value itself, or from an alternate rep.
564        // if from an alternate rep, it will contain the value to select
565        if (e != null){
566            if (log.isDebugEnabled()) {
567                log.debug("Variable = {} start action event cmd={}", label(), e.getActionCommand());
568            }
569            if (!(e.getActionCommand().equals(""))) {
570                // is from alternate rep
571                log.debug("{} action event {} was from alternate rep", label(), e.getActionCommand());
572                _value.setSelectedItem(e.getActionCommand());
573
574                // match and select in tree
575                if (_nstored > 0) {
576                    for (int i = 0; i < _nstored; i++) {
577                        if (e.getActionCommand().equals(_itemArray[i])) {
578                            // now select in the tree
579                            TreePath path = _pathArray[i];
580                            for (JTree tree : trees) {
581                                tree.setSelectionPath(path);
582                                // ensure selection is in visible portion of JScrollPane
583                                tree.scrollPathToVisible(path);
584                            }
585                            break; // first one is enough
586                        }
587                    }
588                }
589            }
590
591            // called for new values - set the CV as needed
592            CvValue cv = _cvMap.get(getCvNum());
593            if (cv == null) {
594                log.error("no CV defined in enumVal {}, skipping setValue", _cvMap.get(getCvName()));
595                return;
596            }
597
598            updatedDropDown();
599
600        }
601        exitField();
602    }
603
604    /**
605     * FocusListener implementations.
606     */
607    @Override
608    public void focusGained(FocusEvent e) {
609        log.debug("Variable={}; focusGained", _name);
610        enterField();
611    }
612
613    @Override
614    public void focusLost(FocusEvent e) {
615        log.debug("Variable={}; focusLost", _name);
616        exitField();
617    }
618
619    // to complete this class, fill in the routines to handle "Value" parameter
620    // and to read/write/hear parameter changes.
621    @Override
622    public String getValueString() {
623        return Integer.toString(getIntValue());
624    }
625
626    /**
627     * Set value from a String value.
628     *
629     * @param value a string representing the Long value to be set
630     */
631    public void setValue(int value) {
632        if(value > 0){
633            try {
634                long longVal = value;
635                long val = longVal;
636                setLongValue(val);
637            } catch (NumberFormatException e) {
638                log.warn("skipping set of non-long value \"{}\"", value);
639            }
640            selectValue(value);
641        }
642    }
643
644    @Override
645    public void setIntValue(int i) {
646        setLongValue(i);
647    }
648
649    @Override
650    public int getIntValue() {
651        if (_value.getSelectedIndex() >= _valueArray.length || _value.getSelectedIndex() < 0) {
652            log.error("trying to get value {} too large for array length {} in var {}", _value.getSelectedIndex(), _valueArray.length, label());
653        }
654        log.debug("SelectedIndex={} value={}", _value.getSelectedIndex(), _valueArray[_value.getSelectedIndex()]);
655        return _valueArray[_value.getSelectedIndex()];
656    }
657
658    /**
659     * Get the value as an unsigned long.
660     *
661     * @return the value as a long
662     */
663    @Override
664    public long getLongValue() {
665        return _valueArray[_value.getSelectedIndex()];
666    }
667
668    @Override
669    public String getTextValue() {
670        if (_value.getSelectedItem() != null) {
671            return _value.getSelectedItem().toString();
672        } else {
673            return "";
674        }
675    }
676
677    @Override
678    public Object getValueObject() {
679        return getLongValue();
680    }
681
682    @Override
683    public Component getCommonRep() {
684        if (getReadOnly()) {
685            JLabel r = new JLabel((String)_value.getSelectedItem());
686            updateRepresentation(r);
687            return r;
688        } else {
689            return _value;
690        }
691    }
692
693    private void addReservedEntry(long value) {
694        log.warn("Variable \"{}\" had to add reserved entry for {}", _name, value);
695        // We can be commanded to a number that hasn't been defined.
696        // But that's OK for certain applications.
697        // When this happens, we add enum values as needed
698        log.debug("Create new item with value {} count was {} in {}", value, _value.getItemCount(), label());
699
700        // lengthen arrays
701        _valueArray = java.util.Arrays.copyOf(_valueArray, _valueArray.length + 1);
702
703        _itemArray = java.util.Arrays.copyOf(_itemArray, _itemArray.length + 1);
704
705        _pathArray = java.util.Arrays.copyOf(_pathArray, _pathArray.length + 1);
706
707        addItem("Reserved value " + value, (int)value);
708
709        // update the JComboBox
710        _value.addItem(_itemArray[_nstored - 1]);
711        _value.setSelectedItem(_itemArray[_nstored - 1]);
712
713        // tell trees to redisplay & select
714        for (JTree tree : trees) {
715            ((DefaultTreeModel) tree.getModel()).reload();
716            tree.setSelectionPath(_pathArray[_nstored - 1]);
717            // ensure selection is in visible portion of JScrollPane
718            tree.scrollPathToVisible(_pathArray[_nstored - 1]);
719        }
720    }
721
722    public void setLongValue(long value) {
723        log.debug("Variable={}; enter setLongValue {}", _name, value);
724        long oldVal;
725        try {
726            oldVal = (Long.parseLong((String)_value.getSelectedItem()) - mOffset) / mFactor;
727        } catch (java.lang.NumberFormatException ex) {
728            oldVal = -999;
729        }
730        log.debug("Variable={}; setValue with new value {} old value {}", _name, value, oldVal);
731
732        int lengthOfArray = this._valueArray.length;
733
734        boolean foundIt = false; // did we find entry? If not, have to add one
735        for (int i = 0; i < lengthOfArray; i++) {
736          if (this._valueArray[i] == value){
737              log.trace("{} setLongValue setSelectedIndex to {}", _name, i);
738              _value.setSelectedIndex(i);
739              foundIt = true;
740          }
741        }
742        if (!foundIt) {
743            addReservedEntry(value);
744        }
745
746        if (oldVal != value || getState() == ValueState.UNKNOWN) {
747            actionPerformed(null);
748        }
749        // TODO PENDING: the code used to fire value * mFactor + mOffset, which is a text representation;
750        // but 'oldValue' was converted back using mOffset / mFactor making those two (new / old)
751        // using different scales. Probably a bug, but it has been there from well before
752        // the extended splitVal. Because of the risk of breaking existing
753        // behaviour somewhere, deferring correction until at least the next test release.
754        prop.firePropertyChange("Value", oldVal, value * mFactor + mOffset);
755        log.debug("Variable={}; exit setLongValue old={} new={}", _name, oldVal, value);
756    }
757
758    Color _defaultColor;
759
760    // implement an abstract member to set colors
761    @Override
762    void setColor(Color c) {
763        if (c != null && _value != null) {
764            _value.setBackground(c);
765            log.debug("Variable={}; Set Color to {}", _name, c.toString());
766        } else if (_value != null) {
767            log.debug("Variable={}; Set Color to defaultColor {}", _name, _defaultColor.toString());
768            _value.setBackground(_defaultColor);
769        }
770
771        // prop.firePropertyChange("Value", null, null);
772    }
773
774    int _columns = 1;
775
776
777       @Override
778    public Component getNewRep(String format) {
779        // sort on format type
780        switch (format) {
781            case "tree":
782                DefaultTreeModel dModel = new DefaultTreeModel(treeNodes.getFirst());
783                JTree dTree = new JTree(dModel);
784                trees.add(dTree);
785                JScrollPane dScroll = new JScrollPane(dTree);
786                dTree.setRootVisible(false);
787                dTree.setShowsRootHandles(true);
788                dTree.setScrollsOnExpand(true);
789                dTree.setExpandsSelectedPaths(true);
790                dTree.getSelectionModel().setSelectionMode(DefaultTreeSelectionModel.SINGLE_TREE_SELECTION);
791                // arrange for only leaf nodes can be selected
792                dTree.addTreeSelectionListener(new TreeSelectionListener() {
793                    @Override
794                    public void valueChanged(TreeSelectionEvent e) {
795                        TreePath[] paths = e.getPaths();
796                        for (TreePath path : paths) {
797                            DefaultMutableTreeNode o = (DefaultMutableTreeNode) path.getLastPathComponent();
798                            if (o.getChildCount() > 0) {
799                                ((JTree) e.getSource()).removeSelectionPath(path);
800                            }
801                        }
802                        // now record selection
803                        if (paths.length >= 1) {
804                            if (paths[0].getLastPathComponent() instanceof SplitEnumVariableValue.TreeLeafNode) {
805                                // update value of Variable
806                                setValue(_valueArray[((SplitEnumVariableValue.TreeLeafNode) paths[0].getLastPathComponent()).index]);
807                            }
808                        }
809                    }
810                });
811                // select initial value
812                TreePath path = _pathArray[_value.getSelectedIndex()];
813                dTree.setSelectionPath(path);
814                // ensure selection is in visible portion of JScrollPane
815                dTree.scrollPathToVisible(path);
816
817                if (getReadOnly() || getInfoOnly()) {
818                    log.error("read only variables cannot use tree format: {}", item());
819                }
820                updateRepresentation(dScroll);
821                return dScroll;
822            default: {
823                // return a new JComboBox representing the same model
824                SplitEnumVariableValue.VarComboBox b = new SplitEnumVariableValue.VarComboBox(_value.getModel(), this);
825                comboVars.add(b);
826                if (getReadOnly() || getInfoOnly()) {
827                    b.setEnabled(false);
828                }
829                updateRepresentation(b);
830                return b;
831            }
832        }
833    }
834
835    /**
836     * Select a specific value in the JComboBox display
837     * or, if need be, create another one
838     * @param value The new numerical value for the complete enum variable.
839     */
840    protected void selectValue(int value) {
841        if (_nstored > 0 && value != 0) {
842            for (int i = 0; i < _nstored; i++) {
843                if (_valueArray[i] == value) {
844                    //found it, select it
845                    log.debug("{}: selectValue sets to {}", _name, i);
846                    _value.setSelectedIndex(i);
847
848                    // now select in the tree
849                    TreePath path = _pathArray[i];
850                    for (JTree tree : trees) {
851                        tree.setSelectionPath(path);
852                        // ensure selection is in visible portion of JScrollPane
853                        tree.scrollPathToVisible(path);
854                    }
855                    return;
856                }
857            }
858        }
859
860        // if we got to here, we need to add a new reserved value entry
861        addReservedEntry(value);
862    }
863
864    java.util.List<Component> reps = new java.util.ArrayList<>();
865
866    public int retry = 0; // counts retrys of a single CV
867
868    int _progState = 0; // coded by the following
869    static final int IDLE = 0;
870    static final int READING_FIRST = 1; // positive values are reading, i.e. 2 is read 2nd CV
871    static final int WRITING_FIRST = -1; // negative values are writing, i.e. -2 is write 2nd CV
872
873    static final int bitCount = Long.bitCount(~0);
874    static final long intMask = Integer.toUnsignedLong(~0);
875
876    /**
877     * Notify the connected CVs of a state change from above
878     *
879     * @param state The new state
880     */
881    @Override
882    public void setCvState(ValueState state) {
883        for (int i = 0; i < cvCount; i++) {
884            cvList.get(i).thisCV.setState(state);
885        }
886    }
887
888    @Override
889    public boolean isChanged() {
890        boolean changed = false;
891        for (int i = 0; i < cvCount; i++) {
892            changed = (changed || considerChanged(cvList.get(i).thisCV));
893        }
894        return changed;
895    }
896
897    @Override
898    public boolean isToRead() {
899        boolean toRead = false;
900        for (int i = 0; i < cvCount; i++) {
901            toRead = (toRead || (cvList.get(i).thisCV).isToRead());
902        }
903        return toRead;
904    }
905
906    @Override
907    public boolean isToWrite() {
908        boolean toWrite = false;
909        for (int i = 0; i < cvCount; i++) {
910            toWrite = (toWrite || (cvList.get(i).thisCV).isToWrite());
911        }
912        return toWrite;
913    }
914
915    @Override
916    public void readChanges() {
917        if (isToRead() && !isChanged()) {
918            log.debug("!!!!!!! unacceptable combination in readChanges: {}", label());
919        }
920        if (isChanged() || isToRead()) {
921            readAll();
922        }
923    }
924
925    @Override
926    public void writeChanges() {
927        if (isToWrite() && !isChanged()) {
928            log.debug("!!!!!! unacceptable combination in writeChanges: {}", label());
929        }
930        if (isChanged() || isToWrite()) {
931            writeAll();
932        }
933    }
934
935    @Override
936    public void readAll() {
937        log.debug("Variable={}; splitVal read() invoked", _name);
938        setToRead(false);
939        setBusy(true);  // will be reset when value changes
940        //super.setState(READ);
941        //_value.setSelectedIndex(0); // start with a clean slate
942        for (int i = 0; i < cvCount; i++) { // mark all Cvs as to be read
943            cvList.get(i).thisCV.setState(ValueState.READ);
944        }
945        //super.setState(READING_FIRST);
946        _progState = READING_FIRST;
947        retry = 0;
948        log.debug("Variable={}; Start CV read", _name);
949        log.debug("    Reading CV={}", cvList.get(0).cvName);
950        (cvList.get(0).thisCV).read(_status); // kick off the read sequence
951    }
952
953    @Override
954    public void writeAll() {
955        log.debug("Variable={}; write() invoked", _name);
956        if (getReadOnly()) {
957            log.error("Variable={}; unexpected write operation when readOnly is set", _name);
958        }
959        setToWrite(false);
960        setBusy(true);  // will be reset when value changes
961        if (_progState != IDLE) {
962            log.warn("Variable={}; Programming state {}, not IDLE, in write()", _name, _progState);
963        }
964
965         for (int i = 0; i < cvCount; i++) { // mark all Cvs as to be written
966            cvList.get(i).thisCV.setState(ValueState.STORED);
967        }
968
969       _progState = WRITING_FIRST;
970        log.debug("Variable={}; Start CV write", _name);
971        log.debug("     Writing CV={}", cvList.get(0).cvName);
972        (cvList.get(0).thisCV).write(_status); // kick off the write sequence
973    }
974
975    /**
976     * Assigns a priority value to a given state.
977     *
978     * @param state State to be converted to a priority value
979     * @return Priority value from state, with UNKNOWN numerically highest
980     */
981    @SuppressFBWarnings(value = {"SF_SWITCH_NO_DEFAULT", "SF_SWITCH_FALLTHROUGH"}, justification = "Intentional fallthrough to produce correct value")
982    int priorityValue(ValueState state) {
983        int value = 0;
984        switch (state) {
985            case UNKNOWN:
986                value++;
987            //$FALL-THROUGH$
988            case DIFFERENT:
989                value++;
990            //$FALL-THROUGH$
991            case EDITED:
992                value++;
993            //$FALL-THROUGH$
994            case FROMFILE:
995                value++;
996            //$FALL-THROUGH$
997            default:
998                //$FALL-THROUGH$
999                return value;
1000        }
1001    }
1002
1003       // handle incoming parameter notification
1004    @Override
1005    public void propertyChange(java.beans.PropertyChangeEvent e) {
1006        // notification from CV; check for Value being changed
1007        log.trace("propertyChange for {} {} _progState = {} from {}", e.getPropertyName(), e.getNewValue(), _progState, e.getSource());
1008        switch (e.getPropertyName()) {
1009            case "Busy":
1010
1011                if (((Boolean) e.getNewValue()).equals(Boolean.FALSE)) {
1012
1013                    // check for expected cv
1014                    if ( (_progState >= READING_FIRST || _progState <= WRITING_FIRST ) && e.getSource() != cvList.get(Math.abs(_progState) - 1).thisCV ) {
1015                        log.trace("From \"{}\" but expected \"{}\", ignoring",
1016                            e.getSource(), cvList.get(Math.abs(_progState) - 1).thisCV );
1017                        break;
1018                    }
1019
1020                    if (_progState >= READING_FIRST){
1021                        ValueState curState = (cvList.get(Math.abs(_progState) - 1).thisCV).getState();
1022                        log.trace("propertyChange Busy _progState={} curState={}", _progState, curState);
1023                        if (curState == ValueState.READ) {   // was the last read successful?
1024                            retry = 0;
1025                            log.debug("   Variable={}; Busy finds ValueState.READ cvCount={}", _name, cvCount);
1026                            if (Math.abs(_progState) < cvCount) {   // read next CV
1027                                _progState++;
1028                                log.debug("Increment _progState to {}, reading CV={}", _progState, cvList.get(Math.abs(_progState) - 1).cvName);
1029                                (cvList.get(Math.abs(_progState) - 1).thisCV).read(_status);
1030                            } else {  // finally done, set not busy
1031                                log.debug("Variable={}; Busy goes false with success READING _progState {}", _name, _progState);
1032                                _progState = IDLE;
1033                                setToRead(false);
1034                                setBusy(false);
1035                            }
1036                        } else {   // read failed
1037                            log.debug("   Variable={}; Busy finds other than ValueState.READ _progState {}", _name, _progState);
1038                            if (retry < RETRY_COUNT) { //have we exhausted retry count?
1039                                retry++;
1040                                // stay on same sequence number for retry, don't update _progState
1041                                (cvList.get(Math.abs(_progState) - 1).thisCV).read(_status);
1042                            } else {
1043                                log.warn("Retry failed for CV{}" ,(cvList.get(Math.abs(_progState) - 1).thisCV).toString());
1044                                _progState = IDLE;
1045                                setToRead(false);
1046                                setBusy(false);
1047                                if (RETRY_COUNT > 0) {
1048                                    for (int i = 0; i < cvCount; i++) { // mark all CVs as unknown otherwise problems may occur
1049                                        cvList.get(i).thisCV.setState(ValueState.UNKNOWN);
1050                                    }
1051                                }
1052                            }
1053                        }
1054                    } else  if (_progState <= WRITING_FIRST) {  // writing CVs
1055                        if ((cvList.get(Math.abs(_progState) - 1).thisCV).getState() == ValueState.STORED) {   // was the last read successful?
1056                            if (Math.abs(_progState) < cvCount) {   // write next CV
1057                                _progState--;
1058                                log.debug("Writing CV={}", cvList.get(Math.abs(_progState) - 1).cvName);
1059                                (cvList.get(Math.abs(_progState) - 1).thisCV).write(_status);
1060                            } else {  // finally done, set not busy
1061                                log.debug("Variable={}; Busy goes false with success WRITING _progState {}", _name, _progState);
1062                                _progState = IDLE;
1063                                setBusy(false);
1064                                setToWrite(false);
1065                            }
1066                        } else {   // write failed we're done!
1067                            log.debug("Variable={}; Busy goes false with failure WRITING _progState {}", _name, _progState);
1068                            _progState = IDLE;
1069                            setToWrite(false);
1070                            setBusy(false);
1071                        }
1072                    }
1073                }
1074                break;
1075            case "State": {
1076                log.debug("Possible {} variable state change due to CV state change, so propagate that", _name);
1077                ValueState varState = getState(); // AbstractValue.SAME;
1078                log.debug("{} variable state was {}", _name, varState.getName());
1079                for (int i = 0; i < cvCount; i++) {
1080                    ValueState state = cvList.get(i).thisCV.getState();
1081                    if (i == 0) {
1082                        varState = state;
1083                    } else if (priorityValue(state) > priorityValue(varState)) {
1084                        varState = ValueState.UNKNOWN; // or should it be = state ?
1085//                        varState = state; // or should it be = state ?
1086                    }
1087                }
1088                setState(varState);
1089                for (JTree tree : trees) {
1090                    tree.setBackground(_value.getBackground());
1091                    //tree.setOpaque(true);
1092                }
1093                log.debug("{} variable state set to {}", _name, varState.getName());
1094                break;
1095            }
1096            case "Value": {
1097                // update value of Variable
1098
1099                //setLongValue(Long.parseLong((String)_value.getSelectedItem()));  // check for duplicate done inside setValue
1100                log.debug("update value of Variable {} cvCount={}", _name, cvCount);
1101
1102                int[] intVals = new int[cvCount];
1103
1104                for (int i = 0; i < cvCount; i++) {
1105                    intVals[i] = (cvList.get(i).thisCV.getValue() & maskValAsInt(cvList.get(i).cvMask)) >>> offsetVal(cvList.get(i).cvMask);
1106                    log.trace("   with intVal[{}] = {}", i, intVals[i]);
1107                }
1108
1109                updateVariableValue(intVals);
1110
1111                log.debug("state change due to CV value change, so propagate that");
1112                ValueState varState = ValueState.SAME;
1113                for (int i = 0; i < cvCount; i++) {
1114                    ValueState state = cvList.get(i).thisCV.getState();
1115                    if (priorityValue(state) > priorityValue(varState)) {
1116                        varState = state;
1117                    }
1118                }
1119                setState(varState);
1120
1121                updatedDropDown();
1122
1123                break;
1124            }
1125            default:
1126                break;
1127        }
1128    }
1129
1130    /* Internal class extends a JComboBox so that its color is consistent with
1131     * an underlying variable
1132     *
1133     * @author Bob Jacobsen   Copyright (C) 2001
1134     * @author tweaked by Jordan McBride Copyright (C) 2021
1135     *
1136     */
1137    public static class VarComboBox extends JComboBox<String> {
1138
1139        VarComboBox(ComboBoxModel<String> m, SplitEnumVariableValue var) {
1140            super(m);
1141            _var = var;
1142            _l = new java.beans.PropertyChangeListener() {
1143                @Override
1144                public void propertyChange(java.beans.PropertyChangeEvent e) {
1145                        log.debug("VarComboBox saw property change: {}", e);
1146                    originalPropertyChanged(e);
1147                }
1148            };
1149            // get the original color right
1150            setBackground(_var._value.getBackground());
1151            setOpaque(true);
1152            // listen for changes to original state
1153            _var.addPropertyChangeListener(_l);
1154        }
1155
1156        SplitEnumVariableValue _var;
1157        transient java.beans.PropertyChangeListener _l = null;
1158
1159        void originalPropertyChanged(java.beans.PropertyChangeEvent e) {
1160            // update this color from original state
1161            if (e.getPropertyName().equals("State")) {
1162                setBackground(_var._value.getBackground());
1163                setOpaque(true);
1164            }
1165        }
1166
1167        public void dispose() {
1168            if (_var != null && _l != null) {
1169                _var.removePropertyChangeListener(_l);
1170            }
1171            _l = null;
1172            _var = null;
1173        }
1174    }
1175
1176    /**
1177     * Class to hold CV parameters for CVs used.
1178     */
1179    static class CvItem {
1180
1181        // class fields
1182        String cvName;
1183        String cvMask;
1184        int startOffset;
1185        CvValue thisCV;
1186
1187        CvItem(String cvNameVal, String cvMaskVal) {
1188            cvName = cvNameVal;
1189            cvMask = cvMaskVal;
1190        }
1191    }
1192
1193// clean up connections when done
1194    @Override
1195    public void dispose() {
1196        log.debug("dispose");
1197
1198        // remove connection to CV
1199        if (_cvMap.get(getCvNum()) == null) {
1200            log.error("no CV defined for variable {}, no listeners to remove", getCvNum());
1201        } else {
1202            _cvMap.get(getCvNum()).removePropertyChangeListener(this);
1203        }
1204        // remove connection to graphical representation
1205        disposeReps();
1206    }
1207
1208        void disposeReps() {
1209        if (_value != null) {
1210            _value.removeActionListener(this);
1211        }
1212        for (int i = 0; i < comboCBs.size(); i++) {
1213            comboCBs.get(i).dispose();
1214        }
1215        for (int i = 0; i < comboVars.size(); i++) {
1216            comboVars.get(i).dispose();
1217        }
1218        for (int i = 0; i < comboRBs.size(); i++) {
1219            comboRBs.get(i).dispose();
1220        }
1221    }
1222
1223    static class TreeLeafNode extends DefaultMutableTreeNode {
1224
1225        TreeLeafNode(String name, int index) {
1226            super(name);
1227            this.index = index;
1228        }
1229
1230        int index;
1231    }
1232
1233
1234
1235    // initialize logging
1236    private static final Logger log = LoggerFactory.getLogger(SplitEnumVariableValue.class
1237            .getName());
1238
1239}