001package jmri.jmrix.openlcb.swing.stleditor;
002
003import java.awt.*;
004import java.awt.event.*;
005import java.io.*;
006import java.util.*;
007import java.util.List;
008import java.util.concurrent.atomic.AtomicInteger;
009import java.util.regex.Pattern;
010import java.nio.file.*;
011
012import java.beans.PropertyChangeEvent;
013import java.beans.PropertyChangeListener;
014
015import javax.swing.*;
016import javax.swing.event.ChangeEvent;
017import javax.swing.event.ListSelectionEvent;
018import javax.swing.filechooser.FileNameExtensionFilter;
019import javax.swing.table.AbstractTableModel;
020
021import jmri.jmrix.can.CanSystemConnectionMemo;
022import jmri.util.FileUtil;
023import jmri.util.swing.JComboBoxUtil;
024import jmri.util.swing.JmriJFileChooser;
025import jmri.util.swing.JmriJOptionPane;
026import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
027
028import static org.openlcb.MimicNodeStore.NodeMemo.UPDATE_PROP_SIMPLE_NODE_IDENT;
029
030import org.apache.commons.csv.CSVFormat;
031import org.apache.commons.csv.CSVParser;
032import org.apache.commons.csv.CSVPrinter;
033import org.apache.commons.csv.CSVRecord;
034
035import org.openlcb.*;
036import org.openlcb.cdi.cmd.*;
037import org.openlcb.cdi.impl.ConfigRepresentation;
038
039
040/**
041 * Panel for editing STL logic.
042 *
043 * The primary mode is a connection to a Tower LCC+Q.  When a node is selected, the data
044 * is transferred to Java lists and displayed using Java tables. If changes are to be retained,
045 * the Store process is invoked which updates the Tower LCC+Q CDI.
046 *
047 * An alternate mode uses CSV files to import and export the data.  This enables offline development.
048 * Since the CDI is loaded automatically when the node is selected, to transfer offline development
049 * is a three step process:  Load the CDI, replace the content with the CSV content and then store
050 * to the CDI.
051 *
052 * A third mode is to load a CDI backup file.  This can then be used with the CSV process for offline work.
053 *
054 * The reboot process has several steps.
055 * <ul>
056 *   <li>The Yes option is selected in the compile needed dialog. This sends the reboot command.</li>
057 *   <li>The RebootListener detects that the reboot is done and does getCompileMessage.</li>
058 *   <li>getCompileMessage does a reload for the first syntax message.</li>
059 *   <li>EntryListener gets the reload done event and calls displayCompileMessage.</li>
060 * </ul>
061 *
062 * @author Dave Sand Copyright (C) 2024
063 * @since 5.7.5
064 */
065public class StlEditorPane extends jmri.util.swing.JmriPanel
066        implements jmri.jmrix.can.swing.CanPanelInterface {
067
068    /**
069     * The STL Editor is dependent on the Tower LCC+Q software version
070     */
071    private static int TOWER_LCC_Q_NODE_VERSION = 106;
072    private static String TOWER_LCC_Q_NODE_VERSION_STRING = "v1.06";
073
074    private CanSystemConnectionMemo _canMemo;
075    private OlcbInterface _iface;
076    private ConfigRepresentation _cdi;
077    private MimicNodeStore _store;
078
079    private boolean _dirty = false;
080    private int _logicRow = -1;     // The last selected row, -1 for none
081    private int _groupRow = 0;
082    private List<String> _csvMessages = new ArrayList<>();
083    private AtomicInteger _storeQueueLength = new AtomicInteger(0);
084    private boolean _compileNeeded = false;
085    private boolean _compileInProgress = false;
086    PropertyChangeListener _entryListener = new EntryListener();
087
088    private String _csvDirectoryPath = "";
089
090    private DefaultComboBoxModel<NodeEntry> _nodeModel = new DefaultComboBoxModel<NodeEntry>();
091    private JComboBox<NodeEntry> _nodeBox;
092
093    private JComboBox<Operator> _operators = new JComboBox<>(Operator.values());
094
095    private List<GroupRow> _groupList = new ArrayList<>();
096    private List<InputRow> _inputList = new ArrayList<>();
097    private List<OutputRow> _outputList = new ArrayList<>();
098    private List<ReceiverRow> _receiverList = new ArrayList<>();
099    private List<TransmitterRow> _transmitterList = new ArrayList<>();
100
101    private JTable _groupTable;
102    private JTable _logicTable;
103    private JTable _inputTable;
104    private JTable _outputTable;
105    private JTable _receiverTable;
106    private JTable _transmitterTable;
107
108    private JTabbedPane _detailTabs;
109
110    private JPanel _editButtons;
111    private JButton _addButton;
112    private JButton _insertButton;
113    private JButton _moveUpButton;
114    private JButton _moveDownButton;
115    private JButton _deleteButton;
116    private JButton _percentButton;
117    private JButton _refreshButton;
118    private JButton _storeButton;
119    private JButton _exportButton;
120    private JButton _importButton;
121
122    private JMenuItem _refreshItem;
123    private JMenuItem _storeItem;
124    private JMenuItem _exportItem;
125    private JMenuItem _importItem;
126    private JMenuItem _loadItem;
127
128    // CDI Names
129    private static String INPUT_NAME = "Logic Inputs.Group I%s(%s).Input Description";
130    private static String INPUT_TRUE = "Logic Inputs.Group I%s(%s).True";
131    private static String INPUT_FALSE = "Logic Inputs.Group I%s(%s).False";
132    private static String OUTPUT_NAME = "Logic Outputs.Group Q%s(%s).Output Description";
133    private static String OUTPUT_TRUE = "Logic Outputs.Group Q%s(%s).True";
134    private static String OUTPUT_FALSE = "Logic Outputs.Group Q%s(%s).False";
135    private static String RECEIVER_NAME = "Track Receivers.Rx Circuit(%s).Remote Mast Description";
136    private static String RECEIVER_EVENT = "Track Receivers.Rx Circuit(%s).Link Address";
137    private static String TRANSMITTER_NAME = "Track Transmitters.Tx Circuit(%s).Track Circuit Description";
138    private static String TRANSMITTER_EVENT = "Track Transmitters.Tx Circuit(%s).Link Address";
139    private static String GROUP_NAME = "Conditionals.Logic(%s).Group Description";
140    private static String GROUP_MULTI_LINE = "Conditionals.Logic(%s).MultiLine";
141    private static String SYNTAX_MESSAGE = "Syntax Messages.Syntax Messages.Message 1";
142
143    // Regex Patterns
144    private static Pattern PARSE_VARIABLE = Pattern.compile("[IQYZM](\\d+)\\.(\\d+)");
145    private static Pattern PARSE_LABEL = Pattern.compile("\\D\\w{0,3}:");
146    private static Pattern PARSE_TIMERWORD = Pattern.compile("W#[0123]#\\d{1,3}");
147    private static Pattern PARSE_TIMERVAR = Pattern.compile("T\\d{1,2}");
148    private static Pattern PARSE_HEXPAIR = Pattern.compile("^[0-9a-fA-F]{2}$");
149    private static Pattern PARSE_VERSION = Pattern.compile("^.*(\\d+)\\.(\\d+)$");
150
151    public StlEditorPane() {
152    }
153
154    @Override
155    public void initComponents(CanSystemConnectionMemo memo) {
156        _canMemo = memo;
157        _iface = memo.get(OlcbInterface.class);
158        _store = memo.get(MimicNodeStore.class);
159
160        // Add to GUI here
161        setLayout(new BorderLayout());
162
163        var footer = new JPanel();
164        footer.setLayout(new BorderLayout());
165
166        _addButton = new JButton(Bundle.getMessage("ButtonAdd"));
167        _insertButton = new JButton(Bundle.getMessage("ButtonInsert"));
168        _moveUpButton = new JButton(Bundle.getMessage("ButtonMoveUp"));
169        _moveDownButton = new JButton(Bundle.getMessage("ButtonMoveDown"));
170        _deleteButton = new JButton(Bundle.getMessage("ButtonDelete"));
171        _percentButton = new JButton("0%");
172        _refreshButton = new JButton(Bundle.getMessage("ButtonRefresh"));
173        _storeButton = new JButton(Bundle.getMessage("ButtonStore"));
174        _exportButton = new JButton(Bundle.getMessage("ButtonExport"));
175        _importButton = new JButton(Bundle.getMessage("ButtonImport"));
176
177        _refreshButton.setEnabled(false);
178        _storeButton.setEnabled(false);
179
180        _addButton.addActionListener(this::pushedAddButton);
181        _insertButton.addActionListener(this::pushedInsertButton);
182        _moveUpButton.addActionListener(this::pushedMoveUpButton);
183        _moveDownButton.addActionListener(this::pushedMoveDownButton);
184        _deleteButton.addActionListener(this::pushedDeleteButton);
185        _percentButton.addActionListener(this::pushedPercentButton);
186        _refreshButton.addActionListener(this::pushedRefreshButton);
187        _storeButton.addActionListener(this::pushedStoreButton);
188        _exportButton.addActionListener(this::pushedExportButton);
189        _importButton.addActionListener(this::pushedImportButton);
190
191        _editButtons = new JPanel();
192        _editButtons.add(_addButton);
193        _editButtons.add(_insertButton);
194        _editButtons.add(_moveUpButton);
195        _editButtons.add(_moveDownButton);
196        _editButtons.add(_deleteButton);
197        _editButtons.add(_percentButton);
198        footer.add(_editButtons, BorderLayout.WEST);
199
200        var dataButtons = new JPanel();
201        dataButtons.add(_importButton);
202        dataButtons.add(_exportButton);
203        dataButtons.add(new JLabel(" | "));
204        dataButtons.add(_refreshButton);
205        dataButtons.add(_storeButton);
206        footer.add(dataButtons, BorderLayout.EAST);
207        add(footer, BorderLayout.SOUTH);
208
209        // Define the node selector which goes in the header
210        var nodeSelector = new JPanel();
211        nodeSelector.setLayout(new FlowLayout());
212
213        _nodeBox = new JComboBox<NodeEntry>(_nodeModel);
214
215        // Load node selector combo box
216        for (MimicNodeStore.NodeMemo nodeMemo : _store.getNodeMemos() ) {
217            newNodeInList(nodeMemo);
218        }
219
220        _nodeBox.addActionListener(this::nodeSelected);
221        JComboBoxUtil.setupComboBoxMaxRows(_nodeBox);
222
223        // Force combo box width
224        var dim = _nodeBox.getPreferredSize();
225        var newDim = new Dimension(400, (int)dim.getHeight());
226        _nodeBox.setPreferredSize(newDim);
227
228        nodeSelector.add(_nodeBox);
229        add(nodeSelector, BorderLayout.NORTH);
230
231        // Define the center section of the window which consists of 5 tabs
232        _detailTabs = new JTabbedPane();
233
234        _detailTabs.add(Bundle.getMessage("ButtonG"), buildLogicPanel());  // NOI18N
235        _detailTabs.add(Bundle.getMessage("ButtonI"), buildInputPanel());  // NOI18N
236        _detailTabs.add(Bundle.getMessage("ButtonQ"), buildOutputPanel());  // NOI18N
237        _detailTabs.add(Bundle.getMessage("ButtonY"), buildReceiverPanel());  // NOI18N
238        _detailTabs.add(Bundle.getMessage("ButtonZ"), buildTransmitterPanel());  // NOI18N
239
240        _detailTabs.addChangeListener(this::tabSelected);
241
242        _detailTabs.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
243
244        add(_detailTabs, BorderLayout.CENTER);
245
246        initalizeLists();
247    }
248
249    // --------------  tab configurations ---------
250
251    private JScrollPane buildGroupPanel() {
252        // Create scroll pane
253        var model = new GroupModel();
254        _groupTable = new JTable(model);
255        var scrollPane = new JScrollPane(_groupTable);
256
257        // resize columns
258        for (int i = 0; i < model.getColumnCount(); i++) {
259            int width = model.getPreferredWidth(i);
260            _groupTable.getColumnModel().getColumn(i).setPreferredWidth(width);
261        }
262
263        _groupTable.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
264
265        var  selectionModel = _groupTable.getSelectionModel();
266        selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
267        selectionModel.addListSelectionListener(this::handleGroupRowSelection);
268
269        return scrollPane;
270    }
271
272    private JSplitPane buildLogicPanel() {
273        // Create scroll pane
274        var model = new LogicModel();
275        _logicTable = new JTable(model);
276        var logicScrollPane = new JScrollPane(_logicTable);
277
278        // resize columns
279        for (int i = 0; i < _logicTable.getColumnCount(); i++) {
280            int width = model.getPreferredWidth(i);
281            _logicTable.getColumnModel().getColumn(i).setPreferredWidth(width);
282        }
283
284        _logicTable.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
285
286        // Use the operators combo box for the operator column
287        var col = _logicTable.getColumnModel().getColumn(1);
288        col.setCellEditor(new DefaultCellEditor(_operators));
289        JComboBoxUtil.setupComboBoxMaxRows(_operators);
290
291        var  selectionModel = _logicTable.getSelectionModel();
292        selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
293        selectionModel.addListSelectionListener(this::handleLogicRowSelection);
294
295        var logicPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, buildGroupPanel(), logicScrollPane);
296        logicPanel.setDividerSize(10);
297        logicPanel.setResizeWeight(.10);
298        logicPanel.setDividerLocation(150);
299
300        return logicPanel;
301    }
302
303    private JScrollPane buildInputPanel() {
304        // Create scroll pane
305        var model = new InputModel();
306        _inputTable = new JTable(model);
307        var scrollPane = new JScrollPane(_inputTable);
308
309        // resize columns
310        for (int i = 0; i < model.getColumnCount(); i++) {
311            int width = model.getPreferredWidth(i);
312            _inputTable.getColumnModel().getColumn(i).setPreferredWidth(width);
313        }
314
315        _inputTable.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
316
317        return scrollPane;
318    }
319
320    private JScrollPane buildOutputPanel() {
321        // Create scroll pane
322        var model = new OutputModel();
323        _outputTable = new JTable(model);
324        var scrollPane = new JScrollPane(_outputTable);
325
326        // resize columns
327        for (int i = 0; i < model.getColumnCount(); i++) {
328            int width = model.getPreferredWidth(i);
329            _outputTable.getColumnModel().getColumn(i).setPreferredWidth(width);
330        }
331
332        _outputTable.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
333
334        return scrollPane;
335    }
336
337    private JScrollPane buildReceiverPanel() {
338        // Create scroll pane
339        var model = new ReceiverModel();
340        _receiverTable = new JTable(model);
341        var scrollPane = new JScrollPane(_receiverTable);
342
343        // resize columns
344        for (int i = 0; i < model.getColumnCount(); i++) {
345            int width = model.getPreferredWidth(i);
346            _receiverTable.getColumnModel().getColumn(i).setPreferredWidth(width);
347        }
348
349        _receiverTable.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
350
351        return scrollPane;
352    }
353
354    private JScrollPane buildTransmitterPanel() {
355        // Create scroll pane
356        var model = new TransmitterModel();
357        _transmitterTable = new JTable(model);
358        var scrollPane = new JScrollPane(_transmitterTable);
359
360        // resize columns
361        for (int i = 0; i < model.getColumnCount(); i++) {
362            int width = model.getPreferredWidth(i);
363            _transmitterTable.getColumnModel().getColumn(i).setPreferredWidth(width);
364        }
365
366        _transmitterTable.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
367
368        return scrollPane;
369    }
370
371    private void tabSelected(ChangeEvent e) {
372        if (_detailTabs.getSelectedIndex() == 0) {
373            _editButtons.setVisible(true);
374        } else {
375            _editButtons.setVisible(false);
376        }
377    }
378
379    // --------------  Initialization ---------
380
381    private void initalizeLists() {
382        // Group List
383        for (int i = 0; i < 16; i++) {
384            _groupList.add(new GroupRow(""));
385        }
386
387        // Input List
388        for (int i = 0; i < 128; i++) {
389            _inputList.add(new InputRow("", "", ""));
390        }
391
392        // Output List
393        for (int i = 0; i < 128; i++) {
394            _outputList.add(new OutputRow("", "", ""));
395        }
396
397        // Receiver List
398        for (int i = 0; i < 16; i++) {
399            _receiverList.add(new ReceiverRow("", ""));
400        }
401
402        // Transmitter List
403        for (int i = 0; i < 16; i++) {
404            _transmitterList.add(new TransmitterRow("", ""));
405        }
406    }
407
408    // --------------  Logic table methods ---------
409
410    private void handleGroupRowSelection(ListSelectionEvent e) {
411        if (!e.getValueIsAdjusting()) {
412            _groupRow = _groupTable.getSelectedRow();
413            _logicTable.revalidate();
414            _logicTable.repaint();
415            pushedPercentButton(null);
416        }
417    }
418
419    private void pushedPercentButton(ActionEvent e) {
420        encode(_groupList.get(_groupRow));
421        _percentButton.setText(_groupList.get(_groupRow).getSize());
422    }
423
424    private void handleLogicRowSelection(ListSelectionEvent e) {
425        if (!e.getValueIsAdjusting()) {
426            _logicRow = _logicTable.getSelectedRow();
427            _moveUpButton.setEnabled(_logicRow > 0);
428            _moveDownButton.setEnabled(_logicRow < _logicTable.getRowCount() - 1);
429        }
430    }
431
432    private void pushedAddButton(ActionEvent e) {
433        var logicList = _groupList.get(_groupRow).getLogicList();
434        logicList.add(new LogicRow("", null, "", ""));
435        _logicRow = logicList.size() - 1;
436        _logicTable.revalidate();
437        _logicTable.setRowSelectionInterval(_logicRow, _logicRow);
438        setDirty(true);
439    }
440
441    private void pushedInsertButton(ActionEvent e) {
442        var logicList = _groupList.get(_groupRow).getLogicList();
443        if (_logicRow >= 0 && _logicRow < logicList.size()) {
444            logicList.add(_logicRow, new LogicRow("", null, "", ""));
445            _logicTable.revalidate();
446            _logicTable.setRowSelectionInterval(_logicRow, _logicRow);
447        }
448        setDirty(true);
449    }
450
451    private void pushedMoveUpButton(ActionEvent e) {
452        var logicList = _groupList.get(_groupRow).getLogicList();
453        if (_logicRow >= 0 && _logicRow < logicList.size()) {
454            var logicRow = logicList.remove(_logicRow);
455            logicList.add(_logicRow - 1, logicRow);
456            _logicRow--;
457            _logicTable.revalidate();
458            _logicTable.setRowSelectionInterval(_logicRow, _logicRow);
459        }
460        setDirty(true);
461    }
462
463    private void pushedMoveDownButton(ActionEvent e) {
464        var logicList = _groupList.get(_groupRow).getLogicList();
465        if (_logicRow >= 0 && _logicRow < logicList.size()) {
466            var logicRow = logicList.remove(_logicRow);
467            logicList.add(_logicRow + 1, logicRow);
468            _logicRow++;
469            _logicTable.revalidate();
470            _logicTable.setRowSelectionInterval(_logicRow, _logicRow);
471        }
472        setDirty(true);
473    }
474
475    private void pushedDeleteButton(ActionEvent e) {
476        var logicList = _groupList.get(_groupRow).getLogicList();
477        if (_logicRow >= 0 && _logicRow < logicList.size()) {
478            logicList.remove(_logicRow);
479            _logicTable.revalidate();
480        }
481        setDirty(true);
482    }
483
484    // --------------  Encode/Decode methods ---------
485
486    private String nameToVariable(String name) {
487        if (name != null && !name.isEmpty()) {
488            if (!name.contains("~")) {
489                // Search input and output tables
490                for (int i = 0; i < 16; i++) {
491                    for (int j = 0; j < 8; j++) {
492                        int row = (i * 8) + j;
493                        if (_inputList.get(row).getName().equals(name)) {
494                            return "I" + i + "." + j;
495                        }
496                    }
497                }
498
499                for (int i = 0; i < 16; i++) {
500                    for (int j = 0; j < 8; j++) {
501                        int row = (i * 8) + j;
502                        if (_outputList.get(row).getName().equals(name)) {
503                            return "Q" + i + "." + j;
504                        }
505                    }
506                }
507                return name;
508
509            } else {
510                // Search receiver and transmitter tables
511                var splitName = name.split("~");
512                var baseName = splitName[0];
513                var aspectName = splitName[1];
514                var aspectNumber = 0;
515                try {
516                    aspectNumber = Integer.parseInt(aspectName);
517                    if (aspectNumber < 0 || aspectNumber > 7) {
518                        warningDialog(Bundle.getMessage("TitleAspect"), Bundle.getMessage("MessageAspect", aspectNumber));
519                        aspectNumber = 0;
520                    }
521                } catch (NumberFormatException e) {
522                    warningDialog(Bundle.getMessage("TitleAspect"), Bundle.getMessage("MessageAspect", aspectName));
523                    aspectNumber = 0;
524                }
525                for (int i = 0; i < 16; i++) {
526                    if (_receiverList.get(i).getName().equals(baseName)) {
527                        return "Y" + i + "." + aspectNumber;
528                    }
529                }
530
531                for (int i = 0; i < 16; i++) {
532                    if (_transmitterList.get(i).getName().equals(baseName)) {
533                        return "Z" + i + "." + aspectNumber;
534                    }
535                }
536                return name;
537            }
538        }
539
540        return null;
541    }
542
543    private String variableToName(String variable) {
544        String name = "";
545
546        if (variable.length() > 1) {
547            var varType = variable.substring(0, 1);
548            var match = PARSE_VARIABLE.matcher(variable);
549            if (match.find() && match.groupCount() == 2) {
550                int first = -1;
551                int second = -1;
552                int row = -1;
553
554                try {
555                    first = Integer.parseInt(match.group(1));
556                    second = Integer.parseInt(match.group(2));
557                } catch (NumberFormatException e) {
558                    warningDialog(Bundle.getMessage("TitleVariable"), Bundle.getMessage("MessageVariable", variable));
559                    return name;
560                }
561
562                switch (varType) {
563                    case "I":
564                        row = (first * 8) + second;
565                        name = _inputList.get(row).getName();
566                        if (name.isEmpty()) {
567                            name = variable;
568                        }
569                        break;
570                    case "Q":
571                        row = (first * 8) + second;
572                        name = _outputList.get(row).getName();
573                        if (name.isEmpty()) {
574                            name = variable;
575                        }
576                        break;
577                    case "Y":
578                        row = first;
579                        name = _receiverList.get(row).getName() + "~" + second;
580                        break;
581                    case "Z":
582                        row = first;
583                        name = _transmitterList.get(row).getName() + "~" + second;
584                        break;
585                    default:
586                        log.error("Variable '{}' has an invalid first letter (IQYZ)", variable);
587               }
588            }
589        }
590
591        return name;
592    }
593
594    private void encode(GroupRow groupRow) {
595        String longLine = "";
596
597        var logicList = groupRow.getLogicList();
598        for (var row : logicList) {
599            var sb = new StringBuilder();
600            var jumpLabel = false;
601
602            if (!row.getLabel().isEmpty()) {
603                sb.append(row.getLabel() + " ");
604            }
605
606            if (row.getOper() != null) {
607                var oper = row.getOper();
608                var operName = oper.name();
609
610                // Fix special enums
611                if (operName.equals("Cp")) {
612                    operName = ")";
613                } else if (operName.equals("EQ")) {
614                    operName = "=";
615                } else if (operName.contains("p")) {
616                    operName = operName.replace("p", "(");
617                }
618
619                if (operName.startsWith("J")) {
620                    jumpLabel =true;
621                }
622                sb.append(operName);
623            }
624
625            if (!row.getName().isEmpty()) {
626                var name = row.getName().trim();
627
628                if (jumpLabel) {
629                    sb.append(" " + name);
630                    jumpLabel = false;
631                } else if (isMemory(name)) {
632                    sb.append(" " + name);
633                } else if (isTimerWord(name)) {
634                    sb.append(" " + name);
635                } else if (isTimerVar(name)) {
636                    sb.append(" " + name);
637                } else {
638                    var variable = nameToVariable(name);
639                    if (variable == null) {
640                        JmriJOptionPane.showMessageDialog(null,
641                                Bundle.getMessage("MessageBadName", groupRow.getName(), name),
642                                Bundle.getMessage("TitleBadName"),
643                                JmriJOptionPane.ERROR_MESSAGE);
644                        log.error("bad name: {}", name);
645                    } else {
646                        sb.append(" " + variable);
647                    }
648                }
649            }
650
651            if (!row.getComment().isEmpty()) {
652                var comment = row.getComment().trim();
653                sb.append(" // " + comment);
654            }
655
656            sb.append("\n");
657
658            longLine = longLine + sb.toString();
659        }
660
661        log.debug("MultiLine: {}", longLine);
662
663        if (longLine.length() < 256) {
664            groupRow.setMultiLine(longLine);
665        } else {
666            var overflow = longLine.substring(255);
667            JmriJOptionPane.showMessageDialog(null,
668                    Bundle.getMessage("MessageOverflow", groupRow.getName(), overflow),
669                    Bundle.getMessage("TitleOverflow"),
670                    JmriJOptionPane.ERROR_MESSAGE);
671            log.error("The line overflowed, content truncated:  {}", overflow);
672        }
673    }
674
675    private boolean isMemory(String name) {
676        var match = PARSE_VARIABLE.matcher(name);
677        return (match.find() && name.startsWith("M"));
678    }
679
680    private boolean isTimerWord(String name) {
681        var match = PARSE_TIMERWORD.matcher(name);
682        return match.find();
683    }
684
685    private boolean isTimerVar(String name) {
686        var match = PARSE_TIMERVAR.matcher(name);
687        return match.find();
688    }
689
690    private void decode(GroupRow groupRow) {
691        String[] lines = groupRow.getMultiLine().split("\\n");
692
693        for (int i = 0; i < lines.length; i++) {
694            if (lines[i].isEmpty()) {
695                continue;
696            }
697            String[] tokens = lines[i].split(" ");
698
699            var label = "";
700            var name = "";
701            var comment = "";
702            Operator oper = null;
703
704            boolean needOperator = true;
705
706            for (int j = 0; j < tokens.length; j++) {
707                var token = tokens[j];
708
709                // Get label
710                if (j == 0) {
711                    var match = PARSE_LABEL.matcher(token);
712                    if (match.find()) {
713                        label = token;
714                        continue;
715                    }
716                }
717
718                // Get operator
719                if (needOperator) {
720                    oper = getEnum(token);
721                    if (oper != null) {
722                        needOperator = false;
723                        continue;
724                    }
725                }
726
727                // Get comment
728                if (token.equals("//")) {
729                    int commentPosition = lines[i].indexOf("//");
730                    comment = lines[i].substring(commentPosition + 3);
731                    break;
732                }
733
734                // Get name
735                if (oper != null) {
736                    if (oper.name().startsWith("J")) {   // Jump label
737                        name = token;
738                    } else if (isMemory(token)) {  // Memory variable
739                        name = token;
740                    } else if (isTimerWord(token)) { // Load timer
741                        name = token;
742                    } else if (isTimerVar(token)) {  // Timer variable
743                        name = token;
744                    } else {
745                        var match = PARSE_VARIABLE.matcher(token);
746                        if (match.find()) {
747                            name = variableToName(token);
748                        } else {
749                            name = token;
750                        }
751                    }
752                }
753            }
754
755            var logic = new LogicRow(label, oper, name, comment);
756            groupRow.getLogicList().add(logic);
757        }
758    }
759
760    private Operator getEnum(String name) {
761        try {
762            var temp = name;
763            if (name.equals("=")) {
764                temp = "EQ";
765            } else if (name.equals(")")) {
766                temp = "Cp";
767            } else if (name.endsWith("(")) {
768                temp = name.replace("(", "p");
769            }
770
771            Operator oper = Enum.valueOf(Operator.class, temp);
772            return oper;
773        } catch (IllegalArgumentException ex) {
774            return null;
775        }
776    }
777
778    // --------------  node methods ---------
779
780    private void nodeSelected(ActionEvent e) {
781        NodeEntry node = (NodeEntry) _nodeBox.getSelectedItem();
782        node.getNodeMemo().addPropertyChangeListener(new RebootListener());
783        log.debug("nodeSelected: {}", node);
784
785        if (isValidNodeVersionNumber(node.getNodeMemo())) {
786            _cdi = _iface.getConfigForNode(node.getNodeID());
787            if (_cdi.getRoot() != null) {
788                loadCdiData();
789            } else {
790                JmriJOptionPane.showMessageDialogNonModal(this,
791                        Bundle.getMessage("MessageCdiLoad", node),
792                        Bundle.getMessage("TitleCdiLoad"),
793                        JmriJOptionPane.INFORMATION_MESSAGE,
794                        null);
795                _cdi.addPropertyChangeListener(new CdiListener());
796            }
797        }
798    }
799
800    public class CdiListener implements PropertyChangeListener {
801        public void propertyChange(PropertyChangeEvent e) {
802            String propertyName = e.getPropertyName();
803            log.debug("CdiListener event = {}", propertyName);
804
805            if (propertyName.equals("UPDATE_CACHE_COMPLETE")) {
806                Window[] windows = Window.getWindows();
807                for (Window window : windows) {
808                    if (window instanceof JDialog) {
809                        JDialog dialog = (JDialog) window;
810                        if (dialog.getTitle().equals(Bundle.getMessage("TitleCdiLoad"))) {
811                            dialog.dispose();
812                        }
813                    }
814                }
815                loadCdiData();
816            }
817        }
818    }
819
820    /**
821     * Listens for a property change that implies a node has been rebooted.
822     * This occurs when the user has selected that the editor should do the reboot to compile the updated logic.
823     * When the updateSimpleNodeIdent event occurs and the compile is in progress it starts the message display process.
824     */
825    public class RebootListener implements PropertyChangeListener {
826        public void propertyChange(PropertyChangeEvent e) {
827            String propertyName = e.getPropertyName();
828            if (_compileInProgress && propertyName.equals("updateSimpleNodeIdent")) {
829                log.debug("The reboot appears to be done");
830                getCompileMessage();
831            }
832        }
833    }
834
835    private void newNodeInList(MimicNodeStore.NodeMemo nodeMemo) {
836        // Filter for Tower LCC+Q
837        NodeID node = nodeMemo.getNodeID();
838        String id = node.toString();
839        log.debug("node id: {}", id);
840        if (!id.startsWith("02.01.57.4")) {
841            return;
842        }
843
844        int i = 0;
845        if (_nodeModel.getIndexOf(nodeMemo.getNodeID()) >= 0) {
846            // already exists. Do nothing.
847            return;
848        }
849        NodeEntry e = new NodeEntry(nodeMemo);
850
851        while ((i < _nodeModel.getSize()) && (_nodeModel.getElementAt(i).compareTo(e) < 0)) {
852            ++i;
853        }
854        _nodeModel.insertElementAt(e, i);
855    }
856
857    private boolean isValidNodeVersionNumber(MimicNodeStore.NodeMemo nodeMemo) {
858        SimpleNodeIdent ident = nodeMemo.getSimpleNodeIdent();
859        String versionString = ident.getSoftwareVersion();
860
861        int version = 0;
862        var match = PARSE_VERSION.matcher(versionString);
863        if (match.find()) {
864            var major = match.group(1);
865            var minor = match.group(2);
866            version = Integer.parseInt(major + minor);
867        }
868
869        if (version < TOWER_LCC_Q_NODE_VERSION) {
870            JmriJOptionPane.showMessageDialog(null,
871                    Bundle.getMessage("MessageVersion",
872                            nodeMemo.getNodeID(),
873                            versionString,
874                            TOWER_LCC_Q_NODE_VERSION_STRING),
875                    Bundle.getMessage("TitleVersion"),
876                    JmriJOptionPane.WARNING_MESSAGE);
877            return false;
878        }
879
880        return true;
881    }
882
883    public class EntryListener implements PropertyChangeListener {
884        public void propertyChange(PropertyChangeEvent e) {
885            String propertyName = e.getPropertyName();
886            log.debug("EntryListener event = {}", propertyName);
887
888            if (propertyName.equals("PENDING_WRITE_COMPLETE")) {
889                int currentLength = _storeQueueLength.decrementAndGet();
890                log.debug("Listener: queue length = {}, source = {}", currentLength, e.getSource());
891
892                var entry = (ConfigRepresentation.CdiEntry) e.getSource();
893                entry.removePropertyChangeListener(_entryListener);
894
895                if (currentLength < 1) {
896                    log.debug("The queue is back to zero which implies the updates are done");
897                    displayStoreDone();
898                }
899            }
900
901            if (_compileInProgress && propertyName.equals("UPDATE_ENTRY_DATA")) {
902                // The refresh of the first syntax message has completed.
903                var entry = (ConfigRepresentation.StringEntry) e.getSource();
904                entry.removePropertyChangeListener(_entryListener);
905                displayCompileMessage(entry.getValue());
906            }
907        }
908    }
909
910    private void displayStoreDone() {
911        _csvMessages.add(Bundle.getMessage("StoreDone"));
912        var msgType = JmriJOptionPane.ERROR_MESSAGE;
913        if (_csvMessages.size() == 1) {
914            msgType = JmriJOptionPane.INFORMATION_MESSAGE;
915        }
916        JmriJOptionPane.showMessageDialog(this,
917                String.join("\n", _csvMessages),
918                Bundle.getMessage("TitleCdiStore"),
919                msgType);
920
921        if (_compileNeeded) {
922            log.debug("Display compile needed message");
923
924            String[] options = {Bundle.getMessage("EditorReboot"), Bundle.getMessage("CdiReboot")};
925            int response = JmriJOptionPane.showOptionDialog(this,
926                    Bundle.getMessage("MessageCdiReboot"),
927                    Bundle.getMessage("TitleCdiReboot"),
928                    JmriJOptionPane.YES_NO_OPTION,
929                    JmriJOptionPane.QUESTION_MESSAGE,
930                    null,
931                    options,
932                    options[0]);
933
934            if (response == JmriJOptionPane.YES_OPTION) {
935                // Set the compile in process and request the reboot.  The completion will be
936                // handed by the RebootListener.
937                _compileInProgress = true;
938                _cdi.getConnection().getDatagramService().
939                        sendData(_cdi.getRemoteNodeID(), new int[] {0x20, 0xA9});
940            }
941        }
942    }
943
944    /**
945     * Get the first syntax message entry, add the entry listener and request a reload (refresh).
946     * The EntryListener will handle the reload event.
947     */
948    private void getCompileMessage() {
949            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(SYNTAX_MESSAGE);
950            entry.addPropertyChangeListener(_entryListener);
951            entry.reload();
952    }
953
954    /**
955     * Turn off the compile in progress and display the syntax message.
956     * @param message The first syntax message.
957     */
958    private void displayCompileMessage(String message) {
959        _compileInProgress = false;
960        JmriJOptionPane.showMessageDialog(this,
961                Bundle.getMessage("MessageCompile", message),
962                Bundle.getMessage("TitleCompile"),
963                JmriJOptionPane.INFORMATION_MESSAGE);
964    }
965
966    // Notifies that the contents of a given entry have changed. This will delete and re-add the
967    // entry to the model, forcing a refresh of the box.
968    public void updateComboBoxModelEntry(NodeEntry nodeEntry) {
969        int idx = _nodeModel.getIndexOf(nodeEntry.getNodeID());
970        if (idx < 0) {
971            return;
972        }
973        NodeEntry last = _nodeModel.getElementAt(idx);
974        if (last != nodeEntry) {
975            // not the same object -- we're talking about an abandoned entry.
976            nodeEntry.dispose();
977            return;
978        }
979        NodeEntry sel = (NodeEntry) _nodeModel.getSelectedItem();
980        _nodeModel.removeElementAt(idx);
981        _nodeModel.insertElementAt(nodeEntry, idx);
982        _nodeModel.setSelectedItem(sel);
983    }
984
985    protected static class NodeEntry implements Comparable<NodeEntry>, PropertyChangeListener {
986        final MimicNodeStore.NodeMemo nodeMemo;
987        String description = "";
988
989        NodeEntry(MimicNodeStore.NodeMemo memo) {
990            this.nodeMemo = memo;
991            memo.addPropertyChangeListener(this);
992            updateDescription();
993        }
994
995        /**
996         * Constructor for prototype display value
997         *
998         * @param description prototype display value
999         */
1000        public NodeEntry(String description) {
1001            this.nodeMemo = null;
1002            this.description = description;
1003        }
1004
1005        public NodeID getNodeID() {
1006            return nodeMemo.getNodeID();
1007        }
1008
1009        MimicNodeStore.NodeMemo getNodeMemo() {
1010            return nodeMemo;
1011        }
1012
1013        private void updateDescription() {
1014            SimpleNodeIdent ident = nodeMemo.getSimpleNodeIdent();
1015            StringBuilder sb = new StringBuilder();
1016            sb.append(nodeMemo.getNodeID().toString());
1017
1018            addToDescription(ident.getUserName(), sb);
1019            addToDescription(ident.getUserDesc(), sb);
1020            if (!ident.getMfgName().isEmpty() || !ident.getModelName().isEmpty()) {
1021                addToDescription(ident.getMfgName() + " " +ident.getModelName(), sb);
1022            }
1023            addToDescription(ident.getSoftwareVersion(), sb);
1024            String newDescription = sb.toString();
1025            if (!description.equals(newDescription)) {
1026                description = newDescription;
1027            }
1028        }
1029
1030        private void addToDescription(String s, StringBuilder sb) {
1031            if (!s.isEmpty()) {
1032                sb.append(" - ");
1033                sb.append(s);
1034            }
1035        }
1036
1037        private long reorder(long n) {
1038            return (n < 0) ? Long.MAX_VALUE - n : Long.MIN_VALUE + n;
1039        }
1040
1041        @Override
1042        public int compareTo(NodeEntry otherEntry) {
1043            long l1 = reorder(getNodeID().toLong());
1044            long l2 = reorder(otherEntry.getNodeID().toLong());
1045            return Long.compare(l1, l2);
1046        }
1047
1048        @Override
1049        public String toString() {
1050            return description;
1051        }
1052
1053        @Override
1054        @SuppressFBWarnings(value = "EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS",
1055                justification = "Purposefully attempting lookup using NodeID argument in model " +
1056                        "vector.")
1057        public boolean equals(Object o) {
1058            if (o instanceof NodeEntry) {
1059                return getNodeID().equals(((NodeEntry) o).getNodeID());
1060            }
1061            if (o instanceof NodeID) {
1062                return getNodeID().equals(o);
1063            }
1064            return false;
1065        }
1066
1067        @Override
1068        public int hashCode() {
1069            return getNodeID().hashCode();
1070        }
1071
1072        @Override
1073        public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
1074            //log.warning("Received model entry update for " + nodeMemo.getNodeID());
1075            if (propertyChangeEvent.getPropertyName().equals(UPDATE_PROP_SIMPLE_NODE_IDENT)) {
1076                updateDescription();
1077            }
1078        }
1079
1080        public void dispose() {
1081            //log.warning("dispose of " + nodeMemo.getNodeID().toString());
1082            nodeMemo.removePropertyChangeListener(this);
1083        }
1084    }
1085
1086    // --------------  load CDI data ---------
1087
1088    private void loadCdiData() {
1089        if (!replaceData()) {
1090            return;
1091        }
1092
1093        // Load data
1094        loadCdiInputs();
1095        loadCdiOutputs();
1096        loadCdiReceivers();
1097        loadCdiTransmitters();
1098        loadCdiGroups();
1099
1100        for (GroupRow row : _groupList) {
1101            decode(row);
1102        }
1103
1104        setDirty(false);
1105
1106        _groupTable.setRowSelectionInterval(0, 0);
1107
1108        _groupTable.repaint();
1109
1110        _exportButton.setEnabled(true);
1111        _refreshButton.setEnabled(true);
1112        _storeButton.setEnabled(true);
1113        _exportItem.setEnabled(true);
1114        _refreshItem.setEnabled(true);
1115        _storeItem.setEnabled(true);
1116    }
1117
1118    private void pushedRefreshButton(ActionEvent e) {
1119        loadCdiData();
1120    }
1121
1122    private void loadCdiGroups() {
1123        for (int i = 0; i < 16; i++) {
1124            var groupRow = _groupList.get(i);
1125            groupRow.clearLogicList();
1126
1127            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(GROUP_NAME, i));
1128            groupRow.setName(entry.getValue());
1129            entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(GROUP_MULTI_LINE, i));
1130            groupRow.setMultiLine(entry.getValue());
1131        }
1132
1133        _groupTable.revalidate();
1134    }
1135
1136    private void loadCdiInputs() {
1137        for (int i = 0; i < 16; i++) {
1138            for (int j = 0; j < 8; j++) {
1139                var inputRow = _inputList.get((i * 8) + j);
1140
1141                var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(INPUT_NAME, i, j));
1142                inputRow.setName(entry.getValue());
1143                var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(INPUT_TRUE, i, j));
1144                inputRow.setEventTrue(event.getValue().toShortString());
1145                event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(INPUT_FALSE, i, j));
1146                inputRow.setEventFalse(event.getValue().toShortString());
1147            }
1148        }
1149        _inputTable.revalidate();
1150    }
1151
1152    private void loadCdiOutputs() {
1153        for (int i = 0; i < 16; i++) {
1154            for (int j = 0; j < 8; j++) {
1155                var outputRow = _outputList.get((i * 8) + j);
1156
1157                var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(OUTPUT_NAME, i, j));
1158                outputRow.setName(entry.getValue());
1159                var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(OUTPUT_TRUE, i, j));
1160                outputRow.setEventTrue(event.getValue().toShortString());
1161                event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(OUTPUT_FALSE, i, j));
1162                outputRow.setEventFalse(event.getValue().toShortString());
1163            }
1164        }
1165        _outputTable.revalidate();
1166    }
1167
1168    private void loadCdiReceivers() {
1169        for (int i = 0; i < 16; i++) {
1170            var receiverRow = _receiverList.get(i);
1171
1172            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(RECEIVER_NAME, i));
1173            receiverRow.setName(entry.getValue());
1174            var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(RECEIVER_EVENT, i));
1175            receiverRow.setEventId(event.getValue().toShortString());
1176        }
1177        _receiverTable.revalidate();
1178    }
1179
1180    private void loadCdiTransmitters() {
1181        for (int i = 0; i < 16; i++) {
1182            var transmitterRow = _transmitterList.get(i);
1183
1184            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(TRANSMITTER_NAME, i));
1185            transmitterRow.setName(entry.getValue());
1186            var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(TRANSMITTER_EVENT, i));
1187            transmitterRow.setEventId(event.getValue().toShortString());
1188        }
1189        _transmitterTable.revalidate();
1190    }
1191
1192    // --------------  store CDI data ---------
1193
1194    private void pushedStoreButton(ActionEvent e) {
1195        _csvMessages.clear();
1196        _compileNeeded = false;
1197        _storeQueueLength.set(0);
1198
1199        // Store CDI data
1200        storeInputs();
1201        storeOutputs();
1202        storeReceivers();
1203        storeTransmitters();
1204        storeGroups();
1205
1206        setDirty(false);
1207    }
1208
1209    private void storeGroups() {
1210        // store the group data
1211        int currentCount = 0;
1212
1213        for (int i = 0; i < 16; i++) {
1214            var row = _groupList.get(i);
1215
1216            // update the group line
1217            encode(row);
1218
1219            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(GROUP_NAME, i));
1220            if (!row.getName().equals(entry.getValue())) {
1221                entry.addPropertyChangeListener(_entryListener);
1222                entry.setValue(row.getName());
1223                currentCount = _storeQueueLength.incrementAndGet();
1224            }
1225
1226            entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(GROUP_MULTI_LINE, i));
1227            if (!row.getMultiLine().equals(entry.getValue())) {
1228                entry.addPropertyChangeListener(_entryListener);
1229                entry.setValue(row.getMultiLine());
1230                currentCount = _storeQueueLength.incrementAndGet();
1231                _compileNeeded = true;
1232            }
1233
1234            log.debug("Group: {}", row.getName());
1235            log.debug("Logic: {}", row.getMultiLine());
1236        }
1237        log.debug("storeGroups count = {}", currentCount);
1238    }
1239
1240    private void storeInputs() {
1241        int currentCount = 0;
1242
1243        for (int i = 0; i < 16; i++) {
1244            for (int j = 0; j < 8; j++) {
1245                var row = _inputList.get((i * 8) + j);
1246
1247                var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(INPUT_NAME, i, j));
1248                if (!row.getName().equals(entry.getValue())) {
1249                    entry.addPropertyChangeListener(_entryListener);
1250                    entry.setValue(row.getName());
1251                    currentCount = _storeQueueLength.incrementAndGet();
1252                }
1253
1254                var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(INPUT_TRUE, i, j));
1255                if (!row.getEventTrue().equals(event.getValue().toShortString())) {
1256                    event.addPropertyChangeListener(_entryListener);
1257                    event.setValue(new EventID(row.getEventTrue()));
1258                    currentCount = _storeQueueLength.incrementAndGet();
1259                }
1260
1261                event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(INPUT_FALSE, i, j));
1262                if (!row.getEventFalse().equals(event.getValue().toShortString())) {
1263                    event.addPropertyChangeListener(_entryListener);
1264                    event.setValue(new EventID(row.getEventFalse()));
1265                    currentCount = _storeQueueLength.incrementAndGet();
1266                }
1267            }
1268        }
1269        log.debug("storeInputs count = {}", currentCount);
1270    }
1271
1272    private void storeOutputs() {
1273        int currentCount = 0;
1274
1275        for (int i = 0; i < 16; i++) {
1276            for (int j = 0; j < 8; j++) {
1277                var row = _outputList.get((i * 8) + j);
1278
1279                var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(OUTPUT_NAME, i, j));
1280                if (!row.getName().equals(entry.getValue())) {
1281                    entry.addPropertyChangeListener(_entryListener);
1282                    entry.setValue(row.getName());
1283                    currentCount = _storeQueueLength.incrementAndGet();
1284                }
1285
1286                var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(OUTPUT_TRUE, i, j));
1287                if (!row.getEventTrue().equals(event.getValue().toShortString())) {
1288                    event.addPropertyChangeListener(_entryListener);
1289                    event.setValue(new EventID(row.getEventTrue()));
1290                    currentCount = _storeQueueLength.incrementAndGet();
1291                }
1292
1293                event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(OUTPUT_FALSE, i, j));
1294                if (!row.getEventFalse().equals(event.getValue().toShortString())) {
1295                    event.addPropertyChangeListener(_entryListener);
1296                    event.setValue(new EventID(row.getEventFalse()));
1297                    currentCount = _storeQueueLength.incrementAndGet();
1298                }
1299            }
1300        }
1301        log.debug("storeOutputs count = {}", currentCount);
1302    }
1303
1304    private void storeReceivers() {
1305        int currentCount = 0;
1306
1307        for (int i = 0; i < 16; i++) {
1308            var row = _receiverList.get(i);
1309
1310            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(RECEIVER_NAME, i));
1311            if (!row.getName().equals(entry.getValue())) {
1312                entry.addPropertyChangeListener(_entryListener);
1313                entry.setValue(row.getName());
1314                currentCount = _storeQueueLength.incrementAndGet();
1315            }
1316
1317            var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(RECEIVER_EVENT, i));
1318            if (!row.getEventId().equals(event.getValue().toShortString())) {
1319                event.addPropertyChangeListener(_entryListener);
1320                event.setValue(new EventID(row.getEventId()));
1321                currentCount = _storeQueueLength.incrementAndGet();
1322            }
1323        }
1324        log.debug("storeReceivers count = {}", currentCount);
1325    }
1326
1327    private void storeTransmitters() {
1328        int currentCount = 0;
1329
1330        for (int i = 0; i < 16; i++) {
1331            var row = _transmitterList.get(i);
1332
1333            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(TRANSMITTER_NAME, i));
1334            if (!row.getName().equals(entry.getValue())) {
1335                entry.addPropertyChangeListener(_entryListener);
1336                entry.setValue(row.getName());
1337                currentCount = _storeQueueLength.incrementAndGet();
1338            }
1339        }
1340        log.debug("storeTransmitters count = {}", currentCount);
1341    }
1342
1343    // --------------  Backup Import ---------
1344
1345    private void loadBackupData(ActionEvent m) {
1346        if (!replaceData()) {
1347            return;
1348        }
1349
1350        var fileChooser = new JmriJFileChooser(FileUtil.getUserFilesPath());
1351        fileChooser.setApproveButtonText(Bundle.getMessage("LoadCdiButton"));
1352        fileChooser.setDialogTitle(Bundle.getMessage("LoadCdiTitle"));
1353        var filter = new FileNameExtensionFilter(Bundle.getMessage("LoadCdiFilter"), "txt");
1354        fileChooser.addChoosableFileFilter(filter);
1355        fileChooser.setFileFilter(filter);
1356
1357        int response = fileChooser.showOpenDialog(this);
1358        if (response == JFileChooser.CANCEL_OPTION) {
1359            return;
1360        }
1361
1362        List<String> lines = null;
1363        try {
1364            lines = Files.readAllLines(Paths.get(fileChooser.getSelectedFile().getAbsolutePath()));
1365        } catch (IOException e) {
1366            log.error("Failed to load file.", e);
1367            return;
1368        }
1369
1370        for (int i = 0; i < lines.size(); i++) {
1371            if (lines.get(i).startsWith("Logic Inputs.Group")) {
1372                loadBackupInputs(i, lines);
1373                i += 128 * 3;
1374            }
1375
1376            if (lines.get(i).startsWith("Logic Outputs.Group")) {
1377                loadBackupOutputs(i, lines);
1378                i += 128 * 3;
1379            }
1380            if (lines.get(i).startsWith("Track Receivers")) {
1381                loadBackupReceivers(i, lines);
1382                i += 16 * 2;
1383            }
1384            if (lines.get(i).startsWith("Track Transmitters")) {
1385                loadBackupTransmitters(i, lines);
1386                i += 16 * 2;
1387            }
1388            if (lines.get(i).startsWith("Conditionals.Logic")) {
1389                loadBackupGroups(i, lines);
1390                i += 16 * 2;
1391            }
1392        }
1393
1394        for (GroupRow row : _groupList) {
1395            decode(row);
1396        }
1397
1398        setDirty(false);
1399        _groupTable.setRowSelectionInterval(0, 0);
1400        _groupTable.repaint();
1401
1402        _exportButton.setEnabled(true);
1403        _exportItem.setEnabled(true);
1404    }
1405
1406    private String getLineValue(String line) {
1407        if (line.endsWith("=")) {
1408            return "";
1409        }
1410        int index = line.indexOf("=");
1411        var newLine = line.substring(index + 1);
1412        newLine = Util.unescapeString(newLine);
1413        return newLine;
1414    }
1415
1416    private void loadBackupInputs(int index, List<String> lines) {
1417        for (int i = 0; i < 128; i++) {
1418            var inputRow = _inputList.get(i);
1419
1420            inputRow.setName(getLineValue(lines.get(index)));
1421            inputRow.setEventTrue(getLineValue(lines.get(index + 1)));
1422            inputRow.setEventFalse(getLineValue(lines.get(index + 2)));
1423            index += 3;
1424        }
1425
1426        _inputTable.revalidate();
1427    }
1428
1429    private void loadBackupOutputs(int index, List<String> lines) {
1430        for (int i = 0; i < 128; i++) {
1431            var outputRow = _outputList.get(i);
1432
1433            outputRow.setName(getLineValue(lines.get(index)));
1434            outputRow.setEventTrue(getLineValue(lines.get(index + 1)));
1435            outputRow.setEventFalse(getLineValue(lines.get(index + 2)));
1436            index += 3;
1437        }
1438
1439        _outputTable.revalidate();
1440    }
1441
1442    private void loadBackupReceivers(int index, List<String> lines) {
1443        for (int i = 0; i < 16; i++) {
1444            var receiverRow = _receiverList.get(i);
1445
1446            receiverRow.setName(getLineValue(lines.get(index)));
1447            receiverRow.setEventId(getLineValue(lines.get(index + 1)));
1448            index += 2;
1449        }
1450
1451        _receiverTable.revalidate();
1452    }
1453
1454    private void loadBackupTransmitters(int index, List<String> lines) {
1455        for (int i = 0; i < 16; i++) {
1456            var transmitterRow = _transmitterList.get(i);
1457
1458            transmitterRow.setName(getLineValue(lines.get(index)));
1459            transmitterRow.setEventId(getLineValue(lines.get(index + 1)));
1460            index += 2;
1461        }
1462
1463        _transmitterTable.revalidate();
1464    }
1465
1466    private void loadBackupGroups(int index, List<String> lines) {
1467        for (int i = 0; i < 16; i++) {
1468            var groupRow = _groupList.get(i);
1469            groupRow.clearLogicList();
1470
1471            groupRow.setName(getLineValue(lines.get(index)));
1472            groupRow.setMultiLine(getLineValue(lines.get(index + 1)));
1473            index += 2;
1474        }
1475
1476        _groupTable.revalidate();
1477        _logicTable.revalidate();
1478    }
1479
1480    // --------------  CSV Import ---------
1481
1482    private void pushedImportButton(ActionEvent e) {
1483        if (!replaceData()) {
1484            return;
1485        }
1486
1487        if (!setCsvDirectoryPath(true)) {
1488            return;
1489        }
1490
1491        _csvMessages.clear();
1492        importCsvData();
1493        setDirty(false);
1494
1495        _exportButton.setEnabled(true);
1496        _exportItem.setEnabled(true);
1497
1498        if (!_csvMessages.isEmpty()) {
1499            JmriJOptionPane.showMessageDialog(this,
1500                    String.join("\n", _csvMessages),
1501                    Bundle.getMessage("TitleCsvImport"),
1502                    JmriJOptionPane.ERROR_MESSAGE);
1503        }
1504    }
1505
1506    private void importCsvData() {
1507        importGroupLogic();
1508        importInputs();
1509        importOutputs();
1510        importReceivers();
1511        importTransmitters();
1512
1513        _groupTable.setRowSelectionInterval(0, 0);
1514
1515        _groupTable.repaint();
1516    }
1517
1518    private void importGroupLogic() {
1519        List<CSVRecord> records = getCsvRecords("group_logic.csv");
1520        if (records.isEmpty()) {
1521            return;
1522        }
1523
1524        var skipHeader = true;
1525        int groupNumber = -1;
1526        for (CSVRecord record : records) {
1527            if (skipHeader) {
1528                skipHeader = false;
1529                continue;
1530            }
1531
1532            List<String> values = new ArrayList<>();
1533            record.forEach(values::add);
1534
1535            if (values.size() == 1) {
1536                // Create Group
1537                groupNumber++;
1538                var groupRow = _groupList.get(groupNumber);
1539                groupRow.setName(values.get(0));
1540                groupRow.setMultiLine("");
1541                groupRow.clearLogicList();
1542            } else if (values.size() == 5) {
1543                var oper = getEnum(values.get(2));
1544                var logicRow = new LogicRow(values.get(1), oper, values.get(3), values.get(4));
1545                _groupList.get(groupNumber).getLogicList().add(logicRow);
1546            } else {
1547                _csvMessages.add(Bundle.getMessage("ImportGroupError", record.toString()));
1548            }
1549        }
1550
1551        _groupTable.revalidate();
1552        _logicTable.revalidate();
1553    }
1554
1555    private void importInputs() {
1556        List<CSVRecord> records = getCsvRecords("inputs.csv");
1557        if (records.isEmpty()) {
1558            return;
1559        }
1560
1561        for (int i = 0; i < 129; i++) {
1562            if (i == 0) {
1563                continue;
1564            }
1565
1566            var record = records.get(i);
1567            List<String> values = new ArrayList<>();
1568            record.forEach(values::add);
1569
1570            if (values.size() == 4) {
1571                var inputRow = _inputList.get(i - 1);
1572                inputRow.setName(values.get(1));
1573                inputRow.setEventTrue(values.get(2));
1574                inputRow.setEventFalse(values.get(3));
1575            } else {
1576                _csvMessages.add(Bundle.getMessage("ImportInputError", record.toString()));
1577            }
1578        }
1579
1580        _inputTable.revalidate();
1581    }
1582
1583    private void importOutputs() {
1584        List<CSVRecord> records = getCsvRecords("outputs.csv");
1585        if (records.isEmpty()) {
1586            return;
1587        }
1588
1589        for (int i = 0; i < 17; i++) {
1590            if (i == 0) {
1591                continue;
1592            }
1593
1594            var record = records.get(i);
1595            List<String> values = new ArrayList<>();
1596            record.forEach(values::add);
1597
1598            if (values.size() == 4) {
1599                var outputRow = _outputList.get(i - 1);
1600                outputRow.setName(values.get(1));
1601                outputRow.setEventTrue(values.get(2));
1602                outputRow.setEventFalse(values.get(3));
1603            } else {
1604                _csvMessages.add(Bundle.getMessage("ImportOuputError", record.toString()));
1605            }
1606        }
1607
1608        _outputTable.revalidate();
1609    }
1610
1611    private void importReceivers() {
1612        List<CSVRecord> records = getCsvRecords("receivers.csv");
1613        if (records.isEmpty()) {
1614            return;
1615        }
1616
1617        for (int i = 0; i < 17; i++) {
1618            if (i == 0) {
1619                continue;
1620            }
1621
1622            var record = records.get(i);
1623            List<String> values = new ArrayList<>();
1624            record.forEach(values::add);
1625
1626            if (values.size() == 3) {
1627                var receiverRow = _receiverList.get(i - 1);
1628                receiverRow.setName(values.get(1));
1629                receiverRow.setEventId(values.get(2));
1630            } else {
1631                _csvMessages.add(Bundle.getMessage("ImportReceiverError", record.toString()));
1632            }
1633        }
1634
1635        _receiverTable.revalidate();
1636    }
1637
1638    private void importTransmitters() {
1639        List<CSVRecord> records = getCsvRecords("transmitters.csv");
1640        if (records.isEmpty()) {
1641            return;
1642        }
1643
1644        for (int i = 0; i < 17; i++) {
1645            if (i == 0) {
1646                continue;
1647            }
1648
1649            var record = records.get(i);
1650            List<String> values = new ArrayList<>();
1651            record.forEach(values::add);
1652
1653            if (values.size() == 3) {
1654                var transmitterRow = _transmitterList.get(i - 1);
1655                transmitterRow.setName(values.get(1));
1656                transmitterRow.setEventId(values.get(2));
1657            } else {
1658                _csvMessages.add(Bundle.getMessage("ImportTransmitterError", record.toString()));
1659            }
1660        }
1661
1662        _transmitterTable.revalidate();
1663    }
1664
1665    private List<CSVRecord> getCsvRecords(String fileName) {
1666        var recordList = new ArrayList<CSVRecord>();
1667        FileReader fileReader;
1668        try {
1669            fileReader = new FileReader(_csvDirectoryPath + fileName);
1670        } catch (FileNotFoundException ex) {
1671            _csvMessages.add(Bundle.getMessage("ImportFileNotFound", fileName));
1672            return recordList;
1673        }
1674
1675        BufferedReader bufferedReader;
1676        CSVParser csvFile;
1677
1678        try {
1679            bufferedReader = new BufferedReader(fileReader);
1680            csvFile = new CSVParser(bufferedReader, CSVFormat.DEFAULT);
1681            recordList.addAll(csvFile.getRecords());
1682            csvFile.close();
1683            bufferedReader.close();
1684            fileReader.close();
1685        } catch (IOException iox) {
1686            _csvMessages.add(Bundle.getMessage("ImportFileIOError", iox.getMessage(), fileName));
1687        }
1688
1689        return recordList;
1690    }
1691
1692    // --------------  CSV Export ---------
1693
1694    private void pushedExportButton(ActionEvent e) {
1695        if (!setCsvDirectoryPath(false)) {
1696            return;
1697        }
1698
1699        _csvMessages.clear();
1700        exportCsvData();
1701        setDirty(false);
1702
1703        _csvMessages.add(Bundle.getMessage("ExportDone"));
1704        var msgType = JmriJOptionPane.ERROR_MESSAGE;
1705        if (_csvMessages.size() == 1) {
1706            msgType = JmriJOptionPane.INFORMATION_MESSAGE;
1707        }
1708        JmriJOptionPane.showMessageDialog(this,
1709                String.join("\n", _csvMessages),
1710                Bundle.getMessage("TitleCsvExport"),
1711                msgType);
1712    }
1713
1714    private void exportCsvData() {
1715        try {
1716            exportGroupLogic();
1717            exportInputs();
1718            exportOutputs();
1719            exportReceivers();
1720            exportTransmitters();
1721        } catch (IOException ex) {
1722            _csvMessages.add(Bundle.getMessage("ExportIOError", ex.getMessage()));
1723        }
1724
1725    }
1726
1727    private void exportGroupLogic() throws IOException {
1728        var fileWriter = new FileWriter(_csvDirectoryPath + "group_logic.csv");
1729        var bufferedWriter = new BufferedWriter(fileWriter);
1730        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
1731
1732        csvFile.printRecord(Bundle.getMessage("GroupName"), Bundle.getMessage("ColumnLabel"),
1733                 Bundle.getMessage("ColumnOper"), Bundle.getMessage("ColumnName"), Bundle.getMessage("ColumnComment"));
1734
1735        for (int i = 0; i < 16; i++) {
1736            var row = _groupList.get(i);
1737            var groupName = row.getName();
1738            csvFile.printRecord(groupName);
1739            var logicRow = row.getLogicList();
1740            for (LogicRow logic : logicRow) {
1741                var operName = logic.getOperName();
1742                csvFile.printRecord("", logic.getLabel(), operName, logic.getName(), logic.getComment());
1743            }
1744        }
1745
1746        // Flush the write buffer and close the file
1747        csvFile.flush();
1748        csvFile.close();
1749    }
1750
1751    private void exportInputs() throws IOException {
1752        var fileWriter = new FileWriter(_csvDirectoryPath + "inputs.csv");
1753        var bufferedWriter = new BufferedWriter(fileWriter);
1754        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
1755
1756        csvFile.printRecord(Bundle.getMessage("ColumnInput"), Bundle.getMessage("ColumnName"),
1757                 Bundle.getMessage("ColumnTrue"), Bundle.getMessage("ColumnFalse"));
1758
1759        for (int i = 0; i < 16; i++) {
1760            for (int j = 0; j < 8; j++) {
1761                var variable = "I" + i + "." + j;
1762                var row = _inputList.get((i * 8) + j);
1763                csvFile.printRecord(variable, row.getName(), row.getEventTrue(), row.getEventFalse());
1764            }
1765        }
1766
1767        // Flush the write buffer and close the file
1768        csvFile.flush();
1769        csvFile.close();
1770    }
1771
1772    private void exportOutputs() throws IOException {
1773        var fileWriter = new FileWriter(_csvDirectoryPath + "outputs.csv");
1774        var bufferedWriter = new BufferedWriter(fileWriter);
1775        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
1776
1777        csvFile.printRecord(Bundle.getMessage("ColumnOutput"), Bundle.getMessage("ColumnName"),
1778                 Bundle.getMessage("ColumnTrue"), Bundle.getMessage("ColumnFalse"));
1779
1780        for (int i = 0; i < 16; i++) {
1781            for (int j = 0; j < 8; j++) {
1782                var variable = "Q" + i + "." + j;
1783                var row = _outputList.get((i * 8) + j);
1784                csvFile.printRecord(variable, row.getName(), row.getEventTrue(), row.getEventFalse());
1785            }
1786        }
1787
1788        // Flush the write buffer and close the file
1789        csvFile.flush();
1790        csvFile.close();
1791    }
1792
1793    private void exportReceivers() throws IOException {
1794        var fileWriter = new FileWriter(_csvDirectoryPath + "receivers.csv");
1795        var bufferedWriter = new BufferedWriter(fileWriter);
1796        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
1797
1798        csvFile.printRecord(Bundle.getMessage("ColumnCircuit"), Bundle.getMessage("ColumnName"),
1799                 Bundle.getMessage("ColumnEventID"));
1800
1801        for (int i = 0; i < 16; i++) {
1802            var variable = "Y" + i;
1803            var row = _receiverList.get(i);
1804            csvFile.printRecord(variable, row.getName(), row.getEventId());
1805        }
1806
1807        // Flush the write buffer and close the file
1808        csvFile.flush();
1809        csvFile.close();
1810    }
1811
1812    private void exportTransmitters() throws IOException {
1813        var fileWriter = new FileWriter(_csvDirectoryPath + "transmitters.csv");
1814        var bufferedWriter = new BufferedWriter(fileWriter);
1815        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
1816
1817        csvFile.printRecord(Bundle.getMessage("ColumnCircuit"), Bundle.getMessage("ColumnName"),
1818                 Bundle.getMessage("ColumnEventID"));
1819
1820        for (int i = 0; i < 16; i++) {
1821            var variable = "Z" + i;
1822            var row = _transmitterList.get(i);
1823            csvFile.printRecord(variable, row.getName(), row.getEventId());
1824        }
1825
1826        // Flush the write buffer and close the file
1827        csvFile.flush();
1828        csvFile.close();
1829    }
1830
1831    /**
1832     * Select the directory that will be used for the CSV file set.
1833     * @param isOpen - True for CSV Import and false for CSV Export.
1834     * @return true if a directory was selected.
1835     */
1836    private boolean setCsvDirectoryPath(boolean isOpen) {
1837        var directoryChooser = new JmriJFileChooser(FileUtil.getUserFilesPath());
1838        directoryChooser.setApproveButtonText(Bundle.getMessage("SelectCsvButton"));
1839        directoryChooser.setDialogTitle(Bundle.getMessage("SelectCsvTitle"));
1840        directoryChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
1841
1842        int response = 0;
1843        if (isOpen) {
1844            response = directoryChooser.showOpenDialog(this);
1845        } else {
1846            response = directoryChooser.showSaveDialog(this);
1847        }
1848        if (response != JFileChooser.APPROVE_OPTION) {
1849            return false;
1850        }
1851        _csvDirectoryPath = directoryChooser.getSelectedFile().getAbsolutePath() + FileUtil.SEPARATOR;
1852
1853        return true;
1854    }
1855
1856    // --------------  Data Utilities ---------
1857
1858    private void setDirty(boolean dirty) {
1859        _dirty = dirty;
1860    }
1861
1862    private boolean isDirty() {
1863        return _dirty;
1864    }
1865
1866    private boolean replaceData() {
1867        if (isDirty()) {
1868            int response = JmriJOptionPane.showConfirmDialog(this,
1869                    Bundle.getMessage("MessageRevert"),
1870                    Bundle.getMessage("TitleRevert"),
1871                    JmriJOptionPane.YES_NO_OPTION);
1872            if (response != JmriJOptionPane.YES_OPTION) {
1873                return false;
1874            }
1875        }
1876        return true;
1877    }
1878
1879    private void warningDialog(String title, String message) {
1880        JmriJOptionPane.showMessageDialog(this,
1881            message,
1882            title,
1883            JmriJOptionPane.WARNING_MESSAGE);
1884    }
1885
1886    // --------------  Data validation ---------
1887
1888    static boolean isLabelValid(String label) {
1889        if (label.isEmpty()) {
1890            return true;
1891        }
1892
1893        var match = PARSE_LABEL.matcher(label);
1894        if (match.find()) {
1895            return true;
1896        }
1897
1898        JmriJOptionPane.showMessageDialog(null,
1899                Bundle.getMessage("MessageLabel", label),
1900                Bundle.getMessage("TitleLabel"),
1901                JmriJOptionPane.ERROR_MESSAGE);
1902        return false;
1903    }
1904
1905    static boolean isEventValid(String event) {
1906        var valid = true;
1907
1908        if (event.isEmpty()) {
1909            return valid;
1910        }
1911
1912        var hexPairs = event.split("\\.");
1913        if (hexPairs.length != 8) {
1914            valid = false;
1915        } else {
1916            for (int i = 0; i < 8; i++) {
1917                var match = PARSE_HEXPAIR.matcher(hexPairs[i]);
1918                if (!match.find()) {
1919                    valid = false;
1920                    break;
1921                }
1922            }
1923        }
1924
1925        if (!valid) {
1926            JmriJOptionPane.showMessageDialog(null,
1927                    Bundle.getMessage("MessageEvent", event),
1928                    Bundle.getMessage("TitleEvent"),
1929                    JmriJOptionPane.ERROR_MESSAGE);
1930            log.error("bad event: {}", event);
1931        }
1932
1933        return valid;
1934    }
1935
1936    // --------------  table lists ---------
1937
1938    /**
1939     * The Group row contains the name and the raw data for one of the 16 groups.
1940     * It also contains the decoded logic for the group in the logic list.
1941     */
1942    static class GroupRow {
1943        String _name;
1944        String _multiLine = "";
1945        List<LogicRow> _logicList = new ArrayList<>();
1946
1947
1948        GroupRow(String name) {
1949            _name = name;
1950        }
1951
1952        String getName() {
1953            return _name;
1954        }
1955
1956        void setName(String newName) {
1957            _name = newName;
1958        }
1959
1960        List<LogicRow> getLogicList() {
1961            return _logicList;
1962        }
1963
1964        void setLogicList(List<LogicRow> logicList) {
1965            _logicList.clear();
1966            _logicList.addAll(logicList);
1967        }
1968
1969        void clearLogicList() {
1970            _logicList.clear();
1971        }
1972
1973        String getMultiLine() {
1974            return _multiLine;
1975        }
1976
1977        void setMultiLine(String newMultiLine) {
1978            _multiLine = newMultiLine.strip();
1979        }
1980
1981        String getSize() {
1982            int size = (_multiLine.length() * 100) / 255;
1983            return String.valueOf(size) + "%";
1984        }
1985    }
1986
1987    /**
1988     * The definition of a logic row
1989     */
1990    static class LogicRow {
1991        String _label;
1992        Operator _oper;
1993        String _name;
1994        String _comment;
1995
1996        LogicRow(String label, Operator oper, String name, String comment) {
1997            _label = label;
1998            _oper = oper;
1999            _name = name;
2000            _comment = comment;
2001        }
2002
2003        String getLabel() {
2004            return _label;
2005        }
2006
2007        void setLabel(String newLabel) {
2008            var label = newLabel.trim();
2009            if (isLabelValid(label)) {
2010                _label = label;
2011            }
2012        }
2013
2014        Operator getOper() {
2015            return _oper;
2016        }
2017
2018        String getOperName() {
2019            if (_oper == null) {
2020                return "";
2021            }
2022
2023            String operName = _oper.name();
2024
2025            // Fix special enums
2026            if (operName.equals("Cp")) {
2027                operName = ")";
2028            } else if (operName.equals("EQ")) {
2029                operName = "=";
2030            } else if (operName.contains("p")) {
2031                operName = operName.replace("p", "(");
2032            }
2033
2034            return operName;
2035        }
2036
2037        void setOper(Operator newOper) {
2038            _oper = newOper;
2039        }
2040
2041        String getName() {
2042            return _name;
2043        }
2044
2045        void setName(String newName) {
2046            _name = newName.trim();
2047        }
2048
2049        String getComment() {
2050            return _comment;
2051        }
2052
2053        void setComment(String newComment) {
2054            _comment = newComment;
2055        }
2056    }
2057
2058    /**
2059     * The name and assigned true and false events for an Input.
2060     */
2061    static class InputRow {
2062        String _name;
2063        String _eventTrue;
2064        String _eventFalse;
2065
2066        InputRow(String name, String eventTrue, String eventFalse) {
2067            _name = name;
2068            _eventTrue = eventTrue;
2069            _eventFalse = eventFalse;
2070        }
2071
2072        String getName() {
2073            return _name;
2074        }
2075
2076        void setName(String newName) {
2077            _name = newName.trim();
2078        }
2079
2080        String getEventTrue() {
2081            if (_eventTrue.length() == 0) return "00.00.00.00.00.00.00.00";
2082            return _eventTrue;
2083        }
2084
2085        void setEventTrue(String newEventTrue) {
2086            var event = newEventTrue.trim();
2087            if (isEventValid(event)) {
2088                _eventTrue = event;
2089            }
2090        }
2091
2092        String getEventFalse() {
2093            if (_eventFalse.length() == 0) return "00.00.00.00.00.00.00.00";
2094            return _eventFalse;
2095        }
2096
2097        void setEventFalse(String newEventFalse) {
2098            var event = newEventFalse.trim();
2099            if (isEventValid(event)) {
2100                _eventFalse = event;
2101            }
2102        }
2103    }
2104
2105    /**
2106     * The name and assigned true and false events for an Output.
2107     */
2108    static class OutputRow {
2109        String _name;
2110        String _eventTrue;
2111        String _eventFalse;
2112
2113        OutputRow(String name, String eventTrue, String eventFalse) {
2114            _name = name;
2115            _eventTrue = eventTrue;
2116            _eventFalse = eventFalse;
2117        }
2118
2119        String getName() {
2120            return _name;
2121        }
2122
2123        void setName(String newName) {
2124            _name = newName.trim();
2125        }
2126
2127        String getEventTrue() {
2128            if (_eventTrue.length() == 0) return "00.00.00.00.00.00.00.00";
2129            return _eventTrue;
2130        }
2131
2132        void setEventTrue(String newEventTrue) {
2133            var event = newEventTrue.trim();
2134            if (isEventValid(event)) {
2135                _eventTrue = event;
2136            }
2137        }
2138
2139        String getEventFalse() {
2140            if (_eventFalse.length() == 0) return "00.00.00.00.00.00.00.00";
2141            return _eventFalse;
2142        }
2143
2144        void setEventFalse(String newEventFalse) {
2145            var event = newEventFalse.trim();
2146            if (isEventValid(event)) {
2147                _eventFalse = event;
2148            }
2149        }
2150    }
2151
2152    /**
2153     * The name and assigned event id for a circuit receiver.
2154     */
2155    static class ReceiverRow {
2156        String _name;
2157        String _eventid;
2158
2159        ReceiverRow(String name, String eventid) {
2160            _name = name;
2161            _eventid = eventid;
2162        }
2163
2164        String getName() {
2165            return _name;
2166        }
2167
2168        void setName(String newName) {
2169            _name = newName.trim();
2170        }
2171
2172        String getEventId() {
2173            if (_eventid.length() == 0) return "00.00.00.00.00.00.00.00";
2174            return _eventid;
2175        }
2176
2177        void setEventId(String newEventid) {
2178            var event = newEventid.trim();
2179            if (isEventValid(event)) {
2180                _eventid = event;
2181            }
2182        }
2183    }
2184
2185    /**
2186     * The name and assigned event id for a circuit transmitter.
2187     */
2188    static class TransmitterRow {
2189        String _name;
2190        String _eventid;
2191
2192        TransmitterRow(String name, String eventid) {
2193            _name = name;
2194            _eventid = eventid;
2195        }
2196
2197        String getName() {
2198            return _name;
2199        }
2200
2201        void setName(String newName) {
2202            _name = newName.trim();
2203        }
2204
2205        String getEventId() {
2206            if (_eventid.length() == 0) return "00.00.00.00.00.00.00.00";
2207            return _eventid;
2208        }
2209
2210        void setEventId(String newEventid) {
2211            var event = newEventid.trim();
2212            if (isEventValid(event)) {
2213                _eventid = event;
2214            }
2215        }
2216    }
2217
2218    // --------------  table models ---------
2219
2220    /**
2221     * TableModel for Group table entries.
2222     */
2223    class GroupModel extends AbstractTableModel {
2224
2225        GroupModel() {
2226        }
2227
2228        public static final int ROW_COLUMN = 0;
2229        public static final int NAME_COLUMN = 1;
2230
2231        @Override
2232        public int getRowCount() {
2233            return _groupList.size();
2234        }
2235
2236        @Override
2237        public int getColumnCount() {
2238            return 2;
2239        }
2240
2241        @Override
2242        public Class<?> getColumnClass(int c) {
2243            return String.class;
2244        }
2245
2246        @Override
2247        public String getColumnName(int col) {
2248            switch (col) {
2249                case ROW_COLUMN:
2250                    return "";
2251                case NAME_COLUMN:
2252                    return Bundle.getMessage("ColumnName");
2253                default:
2254                    return "unknown";  // NOI18N
2255            }
2256        }
2257
2258        @Override
2259        public Object getValueAt(int r, int c) {
2260            switch (c) {
2261                case ROW_COLUMN:
2262                    return r + 1;
2263                case NAME_COLUMN:
2264                    return _groupList.get(r).getName();
2265                default:
2266                    return null;
2267            }
2268        }
2269
2270        @Override
2271        public void setValueAt(Object type, int r, int c) {
2272            switch (c) {
2273                case NAME_COLUMN:
2274                    _groupList.get(r).setName((String) type);
2275                    setDirty(true);
2276                    break;
2277                default:
2278                    break;
2279            }
2280        }
2281
2282        @Override
2283        public boolean isCellEditable(int r, int c) {
2284            return (c == NAME_COLUMN);
2285        }
2286
2287        public int getPreferredWidth(int col) {
2288            switch (col) {
2289                case ROW_COLUMN:
2290                    return new JTextField(4).getPreferredSize().width;
2291                case NAME_COLUMN:
2292                    return new JTextField(20).getPreferredSize().width;
2293                default:
2294                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2295                    return new JTextField(8).getPreferredSize().width;
2296            }
2297        }
2298    }
2299
2300    /**
2301     * TableModel for STL table entries.
2302     */
2303    class LogicModel extends AbstractTableModel {
2304
2305        LogicModel() {
2306        }
2307
2308        public static final int LABEL_COLUMN = 0;
2309        public static final int OPER_COLUMN = 1;
2310        public static final int NAME_COLUMN = 2;
2311        public static final int COMMENT_COLUMN = 3;
2312
2313        @Override
2314        public int getRowCount() {
2315            var logicList = _groupList.get(_groupRow).getLogicList();
2316            return logicList.size();
2317        }
2318
2319        @Override
2320        public int getColumnCount() {
2321            return 4;
2322        }
2323
2324        @Override
2325        public Class<?> getColumnClass(int c) {
2326            if (c == OPER_COLUMN) return JComboBox.class;
2327            return String.class;
2328        }
2329
2330        @Override
2331        public String getColumnName(int col) {
2332            switch (col) {
2333                case LABEL_COLUMN:
2334                    return Bundle.getMessage("ColumnLabel");  // NOI18N
2335                case OPER_COLUMN:
2336                    return Bundle.getMessage("ColumnOper");  // NOI18N
2337                case NAME_COLUMN:
2338                    return Bundle.getMessage("ColumnName");  // NOI18N
2339                case COMMENT_COLUMN:
2340                    return Bundle.getMessage("ColumnComment");  // NOI18N
2341                default:
2342                    return "unknown";  // NOI18N
2343            }
2344        }
2345
2346        @Override
2347        public Object getValueAt(int r, int c) {
2348            var logicList = _groupList.get(_groupRow).getLogicList();
2349            switch (c) {
2350                case LABEL_COLUMN:
2351                    return logicList.get(r).getLabel();
2352                case OPER_COLUMN:
2353                    return logicList.get(r).getOper();
2354                case NAME_COLUMN:
2355                    return logicList.get(r).getName();
2356                case COMMENT_COLUMN:
2357                    return logicList.get(r).getComment();
2358                default:
2359                    return null;
2360            }
2361        }
2362
2363        @Override
2364        public void setValueAt(Object type, int r, int c) {
2365            var logicList = _groupList.get(_groupRow).getLogicList();
2366            switch (c) {
2367                case LABEL_COLUMN:
2368                    logicList.get(r).setLabel((String) type);
2369                    setDirty(true);
2370                    break;
2371                case OPER_COLUMN:
2372                    var z = (Operator) type;
2373                    if (z != null) {
2374                        if (z.name().startsWith("z")) {
2375                            return;
2376                        }
2377                        if (z.name().equals("x0")) {
2378                            logicList.get(r).setOper(null);
2379                            return;
2380                        }
2381                    }
2382                    logicList.get(r).setOper((Operator) type);
2383                    setDirty(true);
2384                    break;
2385                case NAME_COLUMN:
2386                    logicList.get(r).setName((String) type);
2387                    setDirty(true);
2388                    break;
2389                case COMMENT_COLUMN:
2390                    logicList.get(r).setComment((String) type);
2391                    setDirty(true);
2392                    break;
2393                default:
2394                    break;
2395            }
2396        }
2397
2398        @Override
2399        public boolean isCellEditable(int r, int c) {
2400            return true;
2401        }
2402
2403        public int getPreferredWidth(int col) {
2404            switch (col) {
2405                case LABEL_COLUMN:
2406                    return new JTextField(6).getPreferredSize().width;
2407                case OPER_COLUMN:
2408                    return new JTextField(20).getPreferredSize().width;
2409                case NAME_COLUMN:
2410                case COMMENT_COLUMN:
2411                    return new JTextField(40).getPreferredSize().width;
2412                default:
2413                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2414                    return new JTextField(8).getPreferredSize().width;
2415            }
2416        }
2417    }
2418
2419    /**
2420     * TableModel for Input table entries.
2421     */
2422    class InputModel extends AbstractTableModel {
2423
2424        InputModel() {
2425        }
2426
2427        public static final int INPUT_COLUMN = 0;
2428        public static final int NAME_COLUMN = 1;
2429        public static final int TRUE_COLUMN = 2;
2430        public static final int FALSE_COLUMN = 3;
2431
2432        @Override
2433        public int getRowCount() {
2434            return _inputList.size();
2435        }
2436
2437        @Override
2438        public int getColumnCount() {
2439            return 4;
2440        }
2441
2442        @Override
2443        public Class<?> getColumnClass(int c) {
2444            return String.class;
2445        }
2446
2447        @Override
2448        public String getColumnName(int col) {
2449            switch (col) {
2450                case INPUT_COLUMN:
2451                    return Bundle.getMessage("ColumnInput");  // NOI18N
2452                case NAME_COLUMN:
2453                    return Bundle.getMessage("ColumnName");  // NOI18N
2454                case TRUE_COLUMN:
2455                    return Bundle.getMessage("ColumnTrue");  // NOI18N
2456                case FALSE_COLUMN:
2457                    return Bundle.getMessage("ColumnFalse");  // NOI18N
2458                default:
2459                    return "unknown";  // NOI18N
2460            }
2461        }
2462
2463        @Override
2464        public Object getValueAt(int r, int c) {
2465            switch (c) {
2466                case INPUT_COLUMN:
2467                    int grp = r / 8;
2468                    int rem = r % 8;
2469                    return "I" + grp + "." + rem;
2470                case NAME_COLUMN:
2471                    return _inputList.get(r).getName();
2472                case TRUE_COLUMN:
2473                    return _inputList.get(r).getEventTrue();
2474                case FALSE_COLUMN:
2475                    return _inputList.get(r).getEventFalse();
2476                default:
2477                    return null;
2478            }
2479        }
2480
2481        @Override
2482        public void setValueAt(Object type, int r, int c) {
2483            switch (c) {
2484                case NAME_COLUMN:
2485                    _inputList.get(r).setName((String) type);
2486                    setDirty(true);
2487                    break;
2488                case TRUE_COLUMN:
2489                    _inputList.get(r).setEventTrue((String) type);
2490                    setDirty(true);
2491                    break;
2492                case FALSE_COLUMN:
2493                    _inputList.get(r).setEventFalse((String) type);
2494                    setDirty(true);
2495                    break;
2496                default:
2497                    break;
2498            }
2499        }
2500
2501        @Override
2502        public boolean isCellEditable(int r, int c) {
2503            return ((c == NAME_COLUMN) || (c == TRUE_COLUMN) || (c == FALSE_COLUMN));
2504        }
2505
2506        public int getPreferredWidth(int col) {
2507            switch (col) {
2508                case INPUT_COLUMN:
2509                    return new JTextField(6).getPreferredSize().width;
2510                case NAME_COLUMN:
2511                    return new JTextField(50).getPreferredSize().width;
2512                case TRUE_COLUMN:
2513                case FALSE_COLUMN:
2514                    return new JTextField(20).getPreferredSize().width;
2515                default:
2516                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2517                    return new JTextField(8).getPreferredSize().width;
2518            }
2519        }
2520    }
2521
2522    /**
2523     * TableModel for Output table entries.
2524     */
2525    class OutputModel extends AbstractTableModel {
2526        OutputModel() {
2527        }
2528
2529        public static final int OUTPUT_COLUMN = 0;
2530        public static final int NAME_COLUMN = 1;
2531        public static final int TRUE_COLUMN = 2;
2532        public static final int FALSE_COLUMN = 3;
2533
2534        @Override
2535        public int getRowCount() {
2536            return _outputList.size();
2537        }
2538
2539        @Override
2540        public int getColumnCount() {
2541            return 4;
2542        }
2543
2544        @Override
2545        public Class<?> getColumnClass(int c) {
2546            return String.class;
2547        }
2548
2549        @Override
2550        public String getColumnName(int col) {
2551            switch (col) {
2552                case OUTPUT_COLUMN:
2553                    return Bundle.getMessage("ColumnOutput");  // NOI18N
2554                case NAME_COLUMN:
2555                    return Bundle.getMessage("ColumnName");  // NOI18N
2556                case TRUE_COLUMN:
2557                    return Bundle.getMessage("ColumnTrue");  // NOI18N
2558                case FALSE_COLUMN:
2559                    return Bundle.getMessage("ColumnFalse");  // NOI18N
2560                default:
2561                    return "unknown";  // NOI18N
2562            }
2563        }
2564
2565        @Override
2566        public Object getValueAt(int r, int c) {
2567            switch (c) {
2568                case OUTPUT_COLUMN:
2569                    int grp = r / 8;
2570                    int rem = r % 8;
2571                    return "Q" + grp + "." + rem;
2572                case NAME_COLUMN:
2573                    return _outputList.get(r).getName();
2574                case TRUE_COLUMN:
2575                    return _outputList.get(r).getEventTrue();
2576                case FALSE_COLUMN:
2577                    return _outputList.get(r).getEventFalse();
2578                default:
2579                    return null;
2580            }
2581        }
2582
2583        @Override
2584        public void setValueAt(Object type, int r, int c) {
2585            switch (c) {
2586                case NAME_COLUMN:
2587                    _outputList.get(r).setName((String) type);
2588                    setDirty(true);
2589                    break;
2590                case TRUE_COLUMN:
2591                    _outputList.get(r).setEventTrue((String) type);
2592                    setDirty(true);
2593                    break;
2594                case FALSE_COLUMN:
2595                    _outputList.get(r).setEventFalse((String) type);
2596                    setDirty(true);
2597                    break;
2598                default:
2599                    break;
2600            }
2601        }
2602
2603        @Override
2604        public boolean isCellEditable(int r, int c) {
2605            return ((c == NAME_COLUMN) || (c == TRUE_COLUMN) || (c == FALSE_COLUMN));
2606        }
2607
2608        public int getPreferredWidth(int col) {
2609            switch (col) {
2610                case OUTPUT_COLUMN:
2611                    return new JTextField(6).getPreferredSize().width;
2612                case NAME_COLUMN:
2613                    return new JTextField(50).getPreferredSize().width;
2614                case TRUE_COLUMN:
2615                case FALSE_COLUMN:
2616                    return new JTextField(20).getPreferredSize().width;
2617                default:
2618                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2619                    return new JTextField(8).getPreferredSize().width;
2620            }
2621        }
2622    }
2623
2624    /**
2625     * TableModel for circuit receiver table entries.
2626     */
2627    class ReceiverModel extends AbstractTableModel {
2628
2629        ReceiverModel() {
2630        }
2631
2632        public static final int CIRCUIT_COLUMN = 0;
2633        public static final int NAME_COLUMN = 1;
2634        public static final int EVENTID_COLUMN = 2;
2635
2636        @Override
2637        public int getRowCount() {
2638            return _receiverList.size();
2639        }
2640
2641        @Override
2642        public int getColumnCount() {
2643            return 3;
2644        }
2645
2646        @Override
2647        public Class<?> getColumnClass(int c) {
2648            return String.class;
2649        }
2650
2651        @Override
2652        public String getColumnName(int col) {
2653            switch (col) {
2654                case CIRCUIT_COLUMN:
2655                    return Bundle.getMessage("ColumnCircuit");  // NOI18N
2656                case NAME_COLUMN:
2657                    return Bundle.getMessage("ColumnName");  // NOI18N
2658                case EVENTID_COLUMN:
2659                    return Bundle.getMessage("ColumnEventID");  // NOI18N
2660                default:
2661                    return "unknown";  // NOI18N
2662            }
2663        }
2664
2665        @Override
2666        public Object getValueAt(int r, int c) {
2667            switch (c) {
2668                case CIRCUIT_COLUMN:
2669                    return "Y" + r;
2670                case NAME_COLUMN:
2671                    return _receiverList.get(r).getName();
2672                case EVENTID_COLUMN:
2673                    return _receiverList.get(r).getEventId();
2674                default:
2675                    return null;
2676            }
2677        }
2678
2679        @Override
2680        public void setValueAt(Object type, int r, int c) {
2681            switch (c) {
2682                case NAME_COLUMN:
2683                    _receiverList.get(r).setName((String) type);
2684                    setDirty(true);
2685                    break;
2686                case EVENTID_COLUMN:
2687                    _receiverList.get(r).setEventId((String) type);
2688                    setDirty(true);
2689                    break;
2690                default:
2691                    break;
2692            }
2693        }
2694
2695        @Override
2696        public boolean isCellEditable(int r, int c) {
2697            return ((c == NAME_COLUMN) || (c == EVENTID_COLUMN));
2698        }
2699
2700        public int getPreferredWidth(int col) {
2701            switch (col) {
2702                case CIRCUIT_COLUMN:
2703                    return new JTextField(6).getPreferredSize().width;
2704                case NAME_COLUMN:
2705                    return new JTextField(50).getPreferredSize().width;
2706                case EVENTID_COLUMN:
2707                    return new JTextField(20).getPreferredSize().width;
2708                default:
2709                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2710                    return new JTextField(8).getPreferredSize().width;
2711            }
2712        }
2713    }
2714
2715    /**
2716     * TableModel for circuit transmitter table entries.
2717     */
2718    class TransmitterModel extends AbstractTableModel {
2719
2720        TransmitterModel() {
2721        }
2722
2723        public static final int CIRCUIT_COLUMN = 0;
2724        public static final int NAME_COLUMN = 1;
2725        public static final int EVENTID_COLUMN = 2;
2726
2727        @Override
2728        public int getRowCount() {
2729            return _transmitterList.size();
2730        }
2731
2732        @Override
2733        public int getColumnCount() {
2734            return 3;
2735        }
2736
2737        @Override
2738        public Class<?> getColumnClass(int c) {
2739            return String.class;
2740        }
2741
2742        @Override
2743        public String getColumnName(int col) {
2744            switch (col) {
2745                case CIRCUIT_COLUMN:
2746                    return Bundle.getMessage("ColumnCircuit");  // NOI18N
2747                case NAME_COLUMN:
2748                    return Bundle.getMessage("ColumnName");  // NOI18N
2749                case EVENTID_COLUMN:
2750                    return Bundle.getMessage("ColumnEventID");  // NOI18N
2751                default:
2752                    return "unknown";  // NOI18N
2753            }
2754        }
2755
2756        @Override
2757        public Object getValueAt(int r, int c) {
2758            switch (c) {
2759                case CIRCUIT_COLUMN:
2760                    return "Z" + r;
2761                case NAME_COLUMN:
2762                    return _transmitterList.get(r).getName();
2763                case EVENTID_COLUMN:
2764                    return _transmitterList.get(r).getEventId();
2765                default:
2766                    return null;
2767            }
2768        }
2769
2770        @Override
2771        public void setValueAt(Object type, int r, int c) {
2772            switch (c) {
2773                case NAME_COLUMN:
2774                    _transmitterList.get(r).setName((String) type);
2775                    setDirty(true);
2776                    break;
2777                case EVENTID_COLUMN:
2778                    _transmitterList.get(r).setEventId((String) type);
2779                    setDirty(true);
2780                    break;
2781                default:
2782                    break;
2783            }
2784        }
2785
2786        @Override
2787        public boolean isCellEditable(int r, int c) {
2788            return ((c == NAME_COLUMN) || (c == EVENTID_COLUMN));
2789        }
2790
2791        public int getPreferredWidth(int col) {
2792            switch (col) {
2793                case CIRCUIT_COLUMN:
2794                    return new JTextField(6).getPreferredSize().width;
2795                case NAME_COLUMN:
2796                    return new JTextField(50).getPreferredSize().width;
2797                case EVENTID_COLUMN:
2798                    return new JTextField(20).getPreferredSize().width;
2799                default:
2800                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2801                    return new JTextField(8).getPreferredSize().width;
2802            }
2803        }
2804    }
2805
2806    // --------------  Operator Enum ---------
2807
2808    public enum Operator {
2809        x0(Bundle.getMessage("Separator0")),
2810        z1(Bundle.getMessage("Separator1")),
2811        A(Bundle.getMessage("OperatorA")),
2812        AN(Bundle.getMessage("OperatorAN")),
2813        O(Bundle.getMessage("OperatorO")),
2814        ON(Bundle.getMessage("OperatorON")),
2815        X(Bundle.getMessage("OperatorX")),
2816        XN(Bundle.getMessage("OperatorXN")),
2817
2818        z2(Bundle.getMessage("Separator2")),    // The STL parens are represented by lower case p
2819        Ap(Bundle.getMessage("OperatorAp")),
2820        ANp(Bundle.getMessage("OperatorANp")),
2821        Op(Bundle.getMessage("OperatorOp")),
2822        ONp(Bundle.getMessage("OperatorONp")),
2823        Xp(Bundle.getMessage("OperatorXp")),
2824        XNp(Bundle.getMessage("OperatorXNp")),
2825        Cp(Bundle.getMessage("OperatorCp")),    // Close paren
2826
2827        z3(Bundle.getMessage("Separator3")),
2828        EQ(Bundle.getMessage("OperatorEQ")),    // = operator
2829        R(Bundle.getMessage("OperatorR")),
2830        S(Bundle.getMessage("OperatorS")),
2831
2832        z4(Bundle.getMessage("Separator4")),
2833        NOT(Bundle.getMessage("OperatorNOT")),
2834        SET(Bundle.getMessage("OperatorSET")),
2835        CLR(Bundle.getMessage("OperatorCLR")),
2836        SAVE(Bundle.getMessage("OperatorSAVE")),
2837
2838        z5(Bundle.getMessage("Separator5")),
2839        JU(Bundle.getMessage("OperatorJU")),
2840        JC(Bundle.getMessage("OperatorJC")),
2841        JCN(Bundle.getMessage("OperatorJCN")),
2842        JCB(Bundle.getMessage("OperatorJCB")),
2843        JNB(Bundle.getMessage("OperatorJNB")),
2844        JBI(Bundle.getMessage("OperatorJBI")),
2845        JNBI(Bundle.getMessage("OperatorJNBI")),
2846
2847        z6(Bundle.getMessage("Separator6")),
2848        FN(Bundle.getMessage("OperatorFN")),
2849        FP(Bundle.getMessage("OperatorFP")),
2850
2851        z7(Bundle.getMessage("Separator7")),
2852        L(Bundle.getMessage("OperatorL")),
2853        FR(Bundle.getMessage("OperatorFR")),
2854        SP(Bundle.getMessage("OperatorSP")),
2855        SE(Bundle.getMessage("OperatorSE")),
2856        SD(Bundle.getMessage("OperatorSD")),
2857        SS(Bundle.getMessage("OperatorSS")),
2858        SF(Bundle.getMessage("OperatorSF"));
2859
2860        private final String _text;
2861
2862        private Operator(String text) {
2863            this._text = text;
2864        }
2865
2866        @Override
2867        public String toString() {
2868            return _text;
2869        }
2870
2871    }
2872
2873    // --------------  misc items ---------
2874    @Override
2875    public java.util.List<JMenu> getMenus() {
2876        // create a file menu
2877        var retval = new ArrayList<JMenu>();
2878        var fileMenu = new JMenu(Bundle.getMessage("MenuFile"));
2879
2880        _refreshItem = new JMenuItem(Bundle.getMessage("MenuRefresh"));
2881        _storeItem = new JMenuItem(Bundle.getMessage("MenuStore"));
2882        _importItem = new JMenuItem(Bundle.getMessage("MenuImport"));
2883        _exportItem = new JMenuItem(Bundle.getMessage("MenuExport"));
2884        _loadItem = new JMenuItem(Bundle.getMessage("MenuLoad"));
2885
2886        _refreshItem.addActionListener(this::pushedRefreshButton);
2887        _storeItem.addActionListener(this::pushedStoreButton);
2888        _importItem.addActionListener(this::pushedImportButton);
2889        _exportItem.addActionListener(this::pushedExportButton);
2890        _loadItem.addActionListener(this::loadBackupData);
2891
2892        fileMenu.add(_refreshItem);
2893        fileMenu.add(_storeItem);
2894        fileMenu.addSeparator();
2895        fileMenu.add(_importItem);
2896        fileMenu.add(_exportItem);
2897        fileMenu.addSeparator();
2898        fileMenu.add(_loadItem);
2899
2900        _refreshItem.setEnabled(false);
2901        _storeItem.setEnabled(false);
2902        _exportItem.setEnabled(false);
2903
2904        retval.add(fileMenu);
2905        return retval;
2906    }
2907
2908    @Override
2909    public void dispose() {
2910        // and complete this
2911        super.dispose();
2912    }
2913
2914    @Override
2915    public String getHelpTarget() {
2916        return "package.jmri.jmrix.openlcb.swing.stleditor.StlEditorPane";
2917    }
2918
2919    @Override
2920    public String getTitle() {
2921        if (_canMemo != null) {
2922            return (_canMemo.getUserName() + " STL Editor");
2923        }
2924        return Bundle.getMessage("TitleSTLEditor");
2925    }
2926
2927    /**
2928     * Nested class to create one of these using old-style defaults
2929     */
2930    public static class Default extends jmri.jmrix.can.swing.CanNamedPaneAction {
2931
2932        public Default() {
2933            super("STL Editor",
2934                    new jmri.util.swing.sdi.JmriJFrameInterface(),
2935                    StlEditorPane.class.getName(),
2936                    jmri.InstanceManager.getDefault(jmri.jmrix.can.CanSystemConnectionMemo.class));
2937        }
2938    }
2939
2940    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(StlEditorPane.class);
2941}