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