001package jmri.jmrit.symbolicprog;
002
003import java.awt.Color;
004import java.awt.Component;
005import java.awt.event.ActionEvent;
006import java.awt.event.ActionListener;
007import java.util.ArrayDeque;
008import java.util.ArrayList;
009import java.util.Deque;
010import java.util.HashMap;
011import java.util.List;
012import javax.swing.ComboBoxModel;
013import javax.swing.JComboBox;
014import javax.swing.JLabel;
015import javax.swing.JScrollPane;
016import javax.swing.JTree;
017import javax.swing.tree.DefaultMutableTreeNode;
018import javax.swing.tree.DefaultTreeModel;
019import javax.swing.tree.DefaultTreeSelectionModel;
020import javax.swing.tree.TreePath;
021import org.slf4j.Logger;
022import org.slf4j.LoggerFactory;
023
024/**
025 * Extends VariableValue to represent an enumerated variable.
026 * @see VariableValue
027 *
028 * @author Bob Jacobsen Copyright (C) 2001, 2002, 2003, 2013, 2014, 2022
029 */
030public class EnumVariableValue extends VariableValue implements ActionListener {
031
032    public EnumVariableValue(String name, String comment, String cvName,
033            boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly,
034            String cvNum, String mask, int minVal, int maxVal,
035            HashMap<String, CvValue> v, JLabel status, String stdname) {
036        super(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, v, status, stdname);
037        _maxVal = maxVal; // count of possibles in the masked part, i.e. radix mask. Can be higher that the enums count
038        _minVal = minVal;
039
040        treeNodes.addLast(new DefaultMutableTreeNode("")); // root
041        simplifyMask();
042    }
043
044    /**
045     * Create a null object. Normally only used for tests and to pre-load
046     * classes.
047     */
048    public EnumVariableValue() {
049    }
050
051    @Override
052    public CvValue[] usesCVs() {
053        return new CvValue[] {_cvMap.get(getCvNum())};
054    }
055
056    public void nItems(int n) {
057        _itemArray = new String[n];
058        _pathArray = new TreePath[n];
059        _valueArray = new int[n];
060        _nstored = 0;
061        log.debug("enumeration arrays size={}", n);
062    }
063
064    /**
065     * Create a new item in the enumeration, with an associated value one more
066     * than the last item (or zero if this is the first one added)
067     *
068     * @param s Name of the enumeration item
069     */
070    public void addItem(String s) {
071        if (_nstored == 0) {
072            addItem(s, 0);
073        } else {
074            addItem(s, _valueArray[_nstored - 1] + 1);
075        }
076    }
077
078    /**
079     * Create a new item in the enumeration, with a specified associated value.
080     *
081     * @param s Name of the enumeration item
082     * @param value item value.
083     */
084    public void addItem(String s, int value) {
085        _valueArray[_nstored] = value;
086        TreeLeafNode node = new TreeLeafNode(s, _nstored);
087        treeNodes.getLast().add(node);
088        _pathArray[_nstored] = new TreePath(node.getPath());
089        _itemArray[_nstored++] = s;
090        log.debug("_itemArray.length={},_nstored={},s='{}',value={}", _itemArray.length, _nstored, s, value);
091    }
092
093    public void startGroup(String name) {
094        DefaultMutableTreeNode next = new DefaultMutableTreeNode(name);
095        treeNodes.getLast().add(next);
096        treeNodes.addLast(next);
097    }
098
099    public void endGroup() {
100        treeNodes.removeLast();
101    }
102
103    public void lastItem() {
104        _value = new JComboBox<>(java.util.Arrays.copyOf(_itemArray, _nstored));
105        _value.getAccessibleContext().setAccessibleName(label());
106
107        // finish initialization
108        _value.setActionCommand("");
109        _defaultColor = _value.getBackground();
110        _value.setBackground(ValueState.UNKNOWN.getColor());
111        _value.setOpaque(true);
112        // connect to the JComboBox model and the CV so we'll see changes.
113        _value.addActionListener(this);
114        CvValue cv = _cvMap.get(getCvNum());
115        if (cv == null) {
116            log.error("no CV defined in enumVal {}, skipping setState", getCvName());
117            return;
118        }
119        cv.addPropertyChangeListener(this);
120        cv.setState(ValueState.FROMFILE);
121    }
122
123    @Override
124    public void setToolTipText(String t) {
125        super.setToolTipText(t);   // do default stuff
126        _value.setToolTipText(t);  // set our value
127    }
128
129    // stored value
130    JComboBox<String> _value = null;
131
132    // place to keep the items & associated numbers
133    private String[] _itemArray = null;
134    private TreePath[] _pathArray = null;
135    private int[] _valueArray = null;
136    private int _nstored;
137
138    Deque<DefaultMutableTreeNode> treeNodes = new ArrayDeque<>();
139
140    int _maxVal;
141    int _minVal;
142    Color _defaultColor;
143
144    @Override
145    public void setAvailable(boolean a) {
146        _value.setVisible(a);
147        for (ComboCheckBox c : comboCBs) {
148            c.setVisible(a);
149        }
150        for (VarComboBox c : comboVars) {
151            c.setVisible(a);
152        }
153        for (ComboRadioButtons c : comboRBs) {
154            c.setVisible(a);
155        }
156        super.setAvailable(a);
157    }
158
159    @Override
160    public Object rangeVal() {
161        return "enum: " + _minVal + " - " + _maxVal;
162    }
163
164    @Override
165    public void actionPerformed(ActionEvent e) {
166        // see if this is from _value itself, or from an alternate rep.
167        // if from an alternate rep, it will contain the value to select
168        if (log.isDebugEnabled()) {
169            log.debug("{} start action event: {}", label(), e);
170        }
171        if (!(e.getActionCommand().equals(""))) {
172            // is from alternate rep
173            _value.setSelectedItem(e.getActionCommand());
174            if (log.isDebugEnabled()) {
175                log.debug("{} action event was from alternate rep", label());
176            }
177            // match and select in tree
178            if (_nstored > 0) {
179                for (int i = 0; i < _nstored; i++) {
180                    if (e.getActionCommand().equals(_itemArray[i])) {
181                        // now select in the tree
182                        TreePath path = _pathArray[i];
183                        for (JTree tree : trees) {
184                            tree.setSelectionPath(path);
185                            // ensure selection is in visible portion of JScrollPane
186                            tree.scrollPathToVisible(path);
187                        }
188                        break; // first one is enough
189                    }
190                }
191            }
192        }
193
194        int oldVal = getIntValue();
195
196        // called for new values - set the CV as needed
197        CvValue cv = _cvMap.get(getCvNum());
198        if (cv == null) {
199            log.error("no CV defined in enumVal {}, skipping setValue", _cvMap.get(getCvName()));
200            return;
201        }
202        int oldCv = cv.getValue();
203        int newVal = getIntValue();
204        int newCv = setValueInCV(oldCv, newVal, getMask(), _maxVal - 1);
205        if (newCv != oldCv) {
206            cv.setValue(newCv);  // to prevent CV going EDITED during loading of decoder file
207
208            // notify  (this used to be before setting the values)
209            log.debug("{} about to firePropertyChange", label());
210            prop.firePropertyChange("Value", null, oldVal);
211            log.debug("{} returned to from firePropertyChange", label());
212        }
213        log.debug("{} end action event saw oldCv={} newVal={} newCv={}", label(), oldCv, newVal, newCv);
214    }
215
216    // to complete this class, fill in the routines to handle "Value" parameter
217    // and to read/write/hear parameter changes.
218    @Override
219    public String getValueString() {
220        return Integer.toString(getIntValue());
221    }
222
223    @Override
224    public void setIntValue(int i) {
225        // needs to fire Value property as well, as per suggestion by Svata Dedic.
226        setValue(i);
227    }
228
229    @Override
230    public String getTextValue() {
231        if (_value.getSelectedItem() != null) {
232            return _value.getSelectedItem().toString();
233        } else {
234            return "";
235        }
236    }
237
238    @Override
239    public Object getValueObject() {
240        return _value.getSelectedIndex();
241    }
242
243    /**
244     * Set to a specific value.
245     * <p>
246     * This searches for the displayed value, and sets the enum to that
247     * particular one. It used to work off an index, but now it looks for the
248     * value.
249     * <p>
250     * If the value is larger than any defined, a new one is created.
251     * @param value What to set to.
252     */
253    protected void selectValue(int value) {
254        if (_nstored > 0) {
255            for (int i = 0; i < _nstored; i++) {
256                if (_valueArray[i] == value) {
257                    // found it, select it
258                    _value.setSelectedIndex(i);
259
260                    // now select in the tree
261                    TreePath path = _pathArray[i];
262                    for (JTree tree : trees) {
263                        tree.setSelectionPath(path);
264                        // ensure selection is in visible portion of JScrollPane
265                        tree.scrollPathToVisible(path);
266                    }
267                    return;
268                }
269            }
270        }
271
272        // We can be commanded to a number that hasn't been defined.
273        // But that's OK for certain applications.  Instead, we add them as needed
274        log.debug("Create new item with value {} count was {} in {}", value, _value.getItemCount(), label());
275        // lengthen arrays
276        _valueArray = java.util.Arrays.copyOf(_valueArray, _valueArray.length + 1);
277
278        _itemArray = java.util.Arrays.copyOf(_itemArray, _itemArray.length + 1);
279
280        _pathArray = java.util.Arrays.copyOf(_pathArray, _pathArray.length + 1);
281
282        addItem("Reserved value " + value, value);
283
284        // update the JComboBox
285        _value.addItem(_itemArray[_nstored - 1]);
286        _value.setSelectedItem(_itemArray[_nstored - 1]);
287
288        // tell trees to redisplay & select
289        for (JTree tree : trees) {
290            ((DefaultTreeModel) tree.getModel()).reload();
291            tree.setSelectionPath(_pathArray[_nstored - 1]);
292            // ensure selection is in visible portion of JScrollPane
293            tree.scrollPathToVisible(_pathArray[_nstored - 1]);
294        }
295    }
296
297    @Override
298    public int getIntValue() {
299        if (_value.getSelectedIndex() >= _valueArray.length || _value.getSelectedIndex() < 0) {
300            log.error("trying to get value {} too large for array length {} in var {}",
301                    _value.getSelectedIndex(), _valueArray.length, label());
302        }
303        if (log.isDebugEnabled()) {
304            log.debug("SelectedIndex={}, Value={}",
305                    _value.getSelectedIndex(), _valueArray[_value.getSelectedIndex()]);
306        }
307        return _valueArray[_value.getSelectedIndex()];
308    }
309
310    @Override
311    public Component getCommonRep() {
312        return _value;
313    }
314
315    public void setValue(int value) {
316        int oldVal = getIntValue();
317        log.debug("setValue in EnumVariableValue to {}", value);
318        selectValue(value);
319
320        if (oldVal != value || getState() == ValueState.UNKNOWN) {
321            prop.firePropertyChange("Value", null, value);
322        }
323    }
324
325    @Override
326    public Component getNewRep(String format) {
327        // sort on format type
328        switch (format) {
329            case "checkbox": {
330                // this only makes sense if there are exactly two options
331                ComboCheckBox b = new ComboCheckBox(_value, this);
332                b.getAccessibleContext().setAccessibleName(label());
333                comboCBs.add(b);
334                if (getReadOnly() || getInfoOnly()) {
335                    b.setEnabled(false);
336                }
337                updateRepresentation(b);
338                return b;
339            }
340            case "radiobuttons": {
341                ComboRadioButtons b = new ComboRadioButtons(_value, this);
342                b.getAccessibleContext().setAccessibleName(label());
343                comboRBs.add(b);
344                if (getReadOnly() || getInfoOnly()) {
345                    b.setEnabled(false);
346                }
347                updateRepresentation(b);
348                return b;
349            }
350            case "onradiobutton": {
351                ComboRadioButtons b = new ComboOnRadioButton(_value, this);
352                b.getAccessibleContext().setAccessibleName(label());
353                comboRBs.add(b);
354                if (getReadOnly() || getInfoOnly()) {
355                    b.setEnabled(false);
356                }
357                updateRepresentation(b);
358                return b;
359            }
360            case "offradiobutton": {
361                ComboRadioButtons b = new ComboOffRadioButton(_value, this);
362                b.getAccessibleContext().setAccessibleName(label());
363                comboRBs.add(b);
364                if (getReadOnly() || getInfoOnly()) {
365                    b.setEnabled(false);
366                }
367                updateRepresentation(b);
368                return b;
369            }
370            case "tree":
371                DefaultTreeModel dModel = new DefaultTreeModel(treeNodes.getFirst());
372                JTree dTree = new JTree(dModel);
373                trees.add(dTree);
374                JScrollPane dScroll = new JScrollPane(dTree);
375                dTree.setRootVisible(false);
376                dTree.setShowsRootHandles(true);
377                dTree.setScrollsOnExpand(true);
378                dTree.setExpandsSelectedPaths(true);
379                dTree.getSelectionModel().setSelectionMode(DefaultTreeSelectionModel.SINGLE_TREE_SELECTION);
380                // arrange for only leaf nodes can be selected
381                dTree.addTreeSelectionListener(e -> {
382                    TreePath[] paths = e.getPaths();
383                    for (TreePath path : paths) {
384                        DefaultMutableTreeNode o = (DefaultMutableTreeNode) path.getLastPathComponent();
385                        if (o.getChildCount() > 0) {
386                            ((JTree) e.getSource()).removeSelectionPath(path);
387                        }
388                    }
389                    // now record selection
390                    if (paths.length >= 1) {
391                        if (paths[0].getLastPathComponent() instanceof TreeLeafNode) {
392                            // update value of Variable
393                            setValue(_valueArray[((TreeLeafNode) paths[0].getLastPathComponent()).index]);
394                        }
395                    }
396                });
397                // select initial value
398                TreePath path = _pathArray[_value.getSelectedIndex()];
399                dTree.setSelectionPath(path);
400                // ensure selection is in visible portion of JScrollPane
401                dTree.scrollPathToVisible(path);
402
403                if (getReadOnly() || getInfoOnly()) {
404                    log.error("read only variables cannot use tree format: {}", item());
405                }
406                updateRepresentation(dScroll);
407                dScroll.getAccessibleContext().setAccessibleName(label());
408                return dScroll;
409            default: {
410                // return a new JComboBox representing the same model
411                VarComboBox b = new VarComboBox(_value.getModel(), this);
412                b.getAccessibleContext().setAccessibleName(label());
413                comboVars.add(b);
414                if (getReadOnly() || getInfoOnly()) {
415                    b.setEnabled(false);
416                }
417                updateRepresentation(b);
418                return b;
419            }
420        }
421    }
422
423    private final List<ComboCheckBox> comboCBs = new ArrayList<>();
424    private final List<VarComboBox> comboVars = new ArrayList<>();
425    private final List<ComboRadioButtons> comboRBs = new ArrayList<>();
426    private final List<JTree> trees = new ArrayList<>();
427
428    // implement an abstract member to set colors
429    @Override
430    void setColor(Color c) {
431        if (c != null) {
432            _value.setBackground(c);
433        } else {
434            _value.setBackground(_defaultColor);
435        }
436        _value.setOpaque(true);
437    }
438
439    /**
440     * Notify the connected CVs of a state change from above
441     */
442    @Override
443    public void setCvState(ValueState state) {
444        _cvMap.get(getCvNum()).setState(state);
445    }
446
447    @Override
448    public boolean isChanged() {
449        CvValue cv = _cvMap.get(getCvNum());
450        return considerChanged(cv);
451    }
452
453    @Override
454    public void readChanges() {
455        if (isToRead() && !isChanged()) {
456            log.debug("!!!!!!! unacceptable combination in readChanges: {}", label());
457        }
458        if (isChanged() || isToRead()) {
459            readAll();
460        }
461    }
462
463    @Override
464    public void writeChanges() {
465        if (isToWrite() && !isChanged()) {
466            log.debug("!!!!!! unacceptable combination in writeChanges: {}", label());
467        }
468        if (isChanged() || isToWrite()) {
469            writeAll();
470        }
471    }
472
473    @Override
474    public void readAll() {
475        setToRead(false);
476        setBusy(true);  // will be reset when value changes
477        _cvMap.get(getCvNum()).read(_status);
478    }
479
480    @Override
481    public void writeAll() {
482        setToWrite(false);
483        if (getReadOnly() || getInfoOnly()) {
484            log.error("unexpected write operation when readOnly is set");
485        }
486        setBusy(true);  // will be reset when value changes
487        _cvMap.get(getCvNum()).write(_status);
488    }
489
490    // handle incoming parameter notification
491    @Override
492    public void propertyChange(java.beans.PropertyChangeEvent e) {
493        // notification from CV; check for Value being changed
494        switch (e.getPropertyName()) {
495            case "Busy":
496                if (e.getNewValue().equals(Boolean.FALSE)) {
497                    setToRead(false);
498                    setToWrite(false);  // some programming operation just finished
499                    setBusy(false);
500                }
501                break;
502            case "State": {
503                CvValue cv = _cvMap.get(getCvNum());
504                if (cv.getState() == ValueState.STORED) {
505                    setToWrite(false);
506                }
507                if (cv.getState() == ValueState.READ) {
508                    setToRead(false);
509                }
510                setState(cv.getState());
511                for (JTree tree : trees) {
512                    tree.setBackground(_value.getBackground());
513                    //tree.setOpaque(true);
514                }
515                break;
516            }
517            case "Value": {
518                // update value of Variable
519                CvValue cv = _cvMap.get(getCvNum());
520                int newVal = getValueInCV(cv.getValue(), getMask(), _maxVal - 1); // _maxVal value is count of possibles, i.e. radix
521                setValue(newVal);  // check for duplicate done inside setValue
522                break;
523            }
524            default:
525                break;
526        }
527    }
528
529    /* Internal class extends a JComboBox so that its color is consistent with
530     * an underlying variable; we return one of these in getNewRep.
531     * <p>
532     * Unlike similar cases elsewhere, this doesn't have to listen to
533     * value changes.  Those are handled automagically since we're sharing the same
534     * model between this object and the real JComboBox value.
535     *
536     * @author   Bob Jacobsen   Copyright (C) 2001
537     */
538    public static class VarComboBox extends JComboBox<String> {
539
540        VarComboBox(ComboBoxModel<String> m, EnumVariableValue var) {
541            super(m);
542            _var = var;
543            _l = e -> {
544                if (log.isDebugEnabled()) {
545                    log.debug("VarComboBox saw property change: {}", e);
546                }
547                originalPropertyChanged(e);
548            };
549            // get the original color right
550            setBackground(_var._value.getBackground());
551            setOpaque(true);
552            // listen for changes to original state
553            _var.addPropertyChangeListener(_l);
554        }
555
556        EnumVariableValue _var;
557        transient java.beans.PropertyChangeListener _l;
558
559        void originalPropertyChanged(java.beans.PropertyChangeEvent e) {
560            // update this color from original state
561            if (e.getPropertyName().equals("State")) {
562                setBackground(_var._value.getBackground());
563                setOpaque(true);
564            }
565        }
566
567        public void dispose() {
568            if (_var != null && _l != null) {
569                _var.removePropertyChangeListener(_l);
570            }
571            _l = null;
572            _var = null;
573        }
574    }
575
576    // clean up connections when done
577    @Override
578    public void dispose() {
579        log.debug("dispose");
580
581        // remove connection to CV
582        if (_cvMap.get(getCvNum()) == null) {
583            log.error("no CV defined for variable {}, no listeners to remove", getCvNum());
584        } else {
585            _cvMap.get(getCvNum()).removePropertyChangeListener(this);
586        }
587        // remove connection to graphical representation
588        disposeReps();
589    }
590
591    void disposeReps() {
592        if (_value != null) {
593            _value.removeActionListener(this);
594        }
595        for (ComboCheckBox comboCB : comboCBs) {
596            comboCB.dispose();
597        }
598        for (VarComboBox comboVar : comboVars) {
599            comboVar.dispose();
600        }
601        for (ComboRadioButtons comboRB : comboRBs) {
602            comboRB.dispose();
603        }
604    }
605
606    static class TreeLeafNode extends DefaultMutableTreeNode {
607
608        TreeLeafNode(String name, int index) {
609            super(name);
610            this.index = index;
611        }
612
613        int index;
614    }
615
616    // initialize logging
617    private final static Logger log = LoggerFactory.getLogger(EnumVariableValue.class);
618
619}