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