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