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