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