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);  // A space between the letter and n.n is valid
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        _messages.clear();
928        _tokenMap = new TreeMap<>();
929        var line = group.getMultiLine();
930        if (line.length() == 0) {
931            return;
932        }
933
934        // Find label locations
935        log.debug("Find label locations");
936        var matchLabel = PARSE_LABEL.matcher(line);
937        while (matchLabel.find()) {
938            var label = line.substring(matchLabel.start(), matchLabel.end());
939            _tokenMap.put(matchLabel.start(), new Token("Label", label, matchLabel.start(), matchLabel.end()));
940        }
941
942        // Find variable locations and operators
943        log.debug("Find variables and operators");
944        var matchVar = PARSE_VARIABLE.matcher(line);
945        while (matchVar.find()) {
946            var variable = line.substring(matchVar.start(), matchVar.end());
947            _tokenMap.put(matchVar.start(), new Token("Var", variable, matchVar.start(), matchVar.end()));
948            var operToken = findOperator(matchVar.start() - 1, line);
949            if (operToken != null) {
950                _tokenMap.put(operToken.getStart(), operToken);
951            }
952        }
953
954        // Find operators without variables
955        log.debug("Find operators without variables");
956        var matchOper = PARSE_NOVAROPER.matcher(line);
957        while (matchOper.find()) {
958            var oper = line.substring(matchOper.start(), matchOper.end());
959
960            if (isOperInComment(line, matchOper.start())) {
961                continue;
962            }
963
964            if (getEnum(oper) != null) {
965                _tokenMap.put(matchOper.start(), new Token("Oper", oper, matchOper.start(), matchOper.end()));
966            } else {
967                _messages.add(Bundle.getMessage("ErrStandAlone", oper));
968            }
969        }
970
971        // Find jump operators and destinations
972        log.debug("Find jump operators and destinations");
973        var matchJump = PARSE_JUMP.matcher(line);
974        while (matchJump.find()) {
975            var jump = line.substring(matchJump.start(), matchJump.end());
976            if (getEnum(jump) != null && (jump.startsWith("J") || jump.startsWith("j"))) {
977                _tokenMap.put(matchJump.start(), new Token("Oper", jump, matchJump.start(), matchJump.end()));
978
979                // Get the jump destination
980                var matchDest = PARSE_DEST.matcher(line);
981                if (matchDest.find(matchJump.end())) {
982                    var dest = matchDest.group(1);
983                    _tokenMap.put(matchDest.start(), new Token("Dest", dest, matchDest.start(), matchDest.end()));
984                } else {
985                    _messages.add(Bundle.getMessage("ErrJumpDest", jump));
986                }
987            } else {
988                _messages.add(Bundle.getMessage("ErrJumpOper", jump));
989            }
990        }
991
992        // Find timer word locations and load operator
993        log.debug("Find timer word locations and load operators");
994        var matchTimerWord = PARSE_TIMERWORD.matcher(line);
995        while (matchTimerWord.find()) {
996            var timerWord = matchTimerWord.group(1);
997            _tokenMap.put(matchTimerWord.start(), new Token("TimerWord", timerWord, matchTimerWord.start(), matchTimerWord.end()));
998            var operToken = findOperator(matchTimerWord.start() - 1, line);
999            if (operToken != null) {
1000                if (operToken.getName().equals("L") || operToken.getName().equals("l")) {
1001                    _tokenMap.put(operToken.getStart(), operToken);
1002                } else {
1003                    _messages.add(Bundle.getMessage("ErrTimerLoad", operToken.getName()));
1004                }
1005            }
1006        }
1007
1008        // Find timer variable locations and S operators
1009        log.debug("Find timer variable locations and S operators");
1010        var matchTimerVar = PARSE_TIMERVAR.matcher(line);
1011        while (matchTimerVar.find()) {
1012            var timerVar = matchTimerVar.group(1);
1013            _tokenMap.put(matchTimerVar.start(), new Token("TimerVar", timerVar, matchTimerVar.start(), matchTimerVar.end()));
1014            var operToken = findOperator(matchTimerVar.start() - 1, line);
1015            if (operToken != null) {
1016                _tokenMap.put(operToken.getStart(), operToken);
1017            }
1018        }
1019
1020        // Find comment locations
1021        log.debug("Find comment locations");
1022
1023        // Add a newline to capture a comment at the end of the input line.
1024        line = line + "\n";
1025
1026        var matchComment1 = PARSE_COMMENT1.matcher(line);
1027        while (matchComment1.find()) {
1028            var comment = matchComment1.group(1).trim();
1029            _tokenMap.put(matchComment1.start(), new Token("Comment", comment, matchComment1.start(), matchComment1.end()));
1030        }
1031
1032        var matchComment2 = PARSE_COMMENT2.matcher(line);
1033        while (matchComment2.find()) {
1034            var comment = matchComment2.group(1).trim();
1035            _tokenMap.put(matchComment2.start(), new Token("Comment", comment, matchComment2.start(), matchComment2.end()));
1036        }
1037
1038        // Check for overlapping jump destinations and following labels
1039        for (Token token : _tokenMap.values()) {
1040            if (token.getType().equals("Dest")) {
1041                var nextKey = _tokenMap.higherKey(token.getStart());
1042                if (nextKey != null) {
1043                    var nextToken = _tokenMap.get(nextKey);
1044                    if (nextToken.getType().equals("Label")) {
1045                        if (token.getEnd() > nextToken.getStart()) {
1046                            _messages.add(Bundle.getMessage("ErrDestLabel", token.getName(), nextToken.getName()));
1047                        }
1048                    }
1049                }
1050            }
1051        }
1052
1053        if (_messages.size() > 0) {
1054            // Display messages
1055            String msgs = _messages.stream().collect(java.util.stream.Collectors.joining("\n"));
1056            JmriJOptionPane.showMessageDialog(null,
1057                    Bundle.getMessage("MsgParseErr", group.getName(), msgs),
1058                    Bundle.getMessage("TitleParseErr"),
1059                    JmriJOptionPane.ERROR_MESSAGE);
1060        }
1061
1062        // Create token debugging output
1063        if (log.isDebugEnabled()) {
1064            log.debug("Decode line:\n{}", line);
1065            for (Token token : _tokenMap.values()) {
1066                log.debug("  Token = {}", token);
1067            }
1068        }
1069    }
1070
1071    /**
1072     * Starting as the operator location minus one, work backwards to find a valid operator. When
1073     * one is found, create and return the token object.
1074     * @param index The current location in the line.
1075     * @param line The line for the current group.
1076     * @return a token or null.
1077     */
1078    private Token findOperator(int index, String line) {
1079        var sb = new StringBuilder();
1080        int limit = 10;
1081
1082        while (limit > 0 && index >= 0) {
1083            var ch = line.charAt(index);
1084            if (ch != ' ') {
1085                sb.insert(0, ch);
1086                if (getEnum(sb.toString()) != null) {
1087                    String oper = sb.toString();
1088                    return new Token("Oper", oper, index, index + oper.length());
1089                }
1090            }
1091            limit--;
1092            index--;
1093        }
1094
1095        // Format error message
1096        int subStart = index < 0 ? 0 : index;
1097        int subEnd = subStart + 20;
1098        if (subEnd > line.length()) {
1099            subEnd = line.length();
1100        }
1101        String fragment = line.substring(subStart, subEnd).replace("\n", "~");
1102        String msg = Bundle.getMessage("ErrNoOper", index, fragment);
1103        _messages.add(msg);
1104        log.error(msg);
1105
1106        return null;
1107    }
1108
1109    /**
1110     * Look backwards in the line for the beginning of a comment.  This is not a precise check.
1111     * @param line The line that contains the Operator.
1112     * @param index The offset of the operator.
1113     * @return true if the operator appears to be in a comment.
1114     */
1115    private boolean isOperInComment(String line, int index) {
1116        int limit = 20;     // look back 20 characters
1117        char previous = 0;
1118
1119        while (limit > 0 && index >= 0) {
1120            var ch = line.charAt(index);
1121
1122            if (ch == 10) {
1123                // Found the end of a previous statement, new line character.
1124                return false;
1125            }
1126
1127            if (ch == '*' && previous == '/') {
1128                // Found the end of a previous /*...*/ comment
1129                return false;
1130            }
1131
1132            if (ch == '/' && (previous == '/' || previous == '*')) {
1133                // Found the start of a comment
1134                return true;
1135            }
1136
1137            previous = ch;
1138            index--;
1139            limit--;
1140        }
1141        return false;
1142    }
1143
1144    private Operator getEnum(String name) {
1145        try {
1146            var temp = name.toUpperCase();
1147            if (name.equals("=")) {
1148                temp = "EQ";
1149            } else if (name.equals(")")) {
1150                temp = "Cp";
1151            } else if (name.endsWith("(")) {
1152                temp = name.toUpperCase().replace("(", "p");
1153            }
1154
1155            Operator oper = Enum.valueOf(Operator.class, temp);
1156            return oper;
1157        } catch (IllegalArgumentException ex) {
1158            return null;
1159        }
1160    }
1161
1162    // --------------  node methods ---------
1163
1164    private void nodeSelected(ActionEvent e) {
1165        NodeEntry node = (NodeEntry) _nodeBox.getSelectedItem();
1166        node.getNodeMemo().addPropertyChangeListener(new RebootListener());
1167        log.debug("nodeSelected: {}", node);
1168
1169        if (isValidNodeVersionNumber(node.getNodeMemo())) {
1170            _cdi = _iface.getConfigForNode(node.getNodeID());
1171            // make sure that the EventNameStore is present
1172            _cdi.eventNameStore = _canMemo.get(OlcbEventNameStore.class);
1173
1174            if (_cdi.getRoot() != null) {
1175                loadCdiData();
1176            } else {
1177                JmriJOptionPane.showMessageDialogNonModal(this,
1178                        Bundle.getMessage("MessageCdiLoad", node),
1179                        Bundle.getMessage("TitleCdiLoad"),
1180                        JmriJOptionPane.INFORMATION_MESSAGE,
1181                        null);
1182                _cdi.addPropertyChangeListener(new CdiListener());
1183            }
1184        }
1185    }
1186
1187    public class CdiListener implements PropertyChangeListener {
1188        @Override
1189        public void propertyChange(PropertyChangeEvent e) {
1190            String propertyName = e.getPropertyName();
1191            log.debug("CdiListener event = {}", propertyName);
1192
1193            if (propertyName.equals("UPDATE_CACHE_COMPLETE")) {
1194                Window[] windows = Window.getWindows();
1195                for (Window window : windows) {
1196                    if (window instanceof JDialog) {
1197                        JDialog dialog = (JDialog) window;
1198                        if (Bundle.getMessage("TitleCdiLoad").equals(dialog.getTitle())) {
1199                            dialog.dispose();
1200                        }
1201                    }
1202                }
1203                loadCdiData();
1204            }
1205        }
1206    }
1207
1208    /**
1209     * Listens for a property change that implies a node has been rebooted.
1210     * This occurs when the user has selected that the editor should do the reboot to compile the updated logic.
1211     * When the updateSimpleNodeIdent event occurs and the compile is in progress it starts the message display process.
1212     */
1213    public class RebootListener implements PropertyChangeListener {
1214        @Override
1215        public void propertyChange(PropertyChangeEvent e) {
1216            String propertyName = e.getPropertyName();
1217            if (_compileInProgress && propertyName.equals("updateSimpleNodeIdent")) {
1218                log.debug("The reboot appears to be done");
1219                getCompileMessage();
1220            }
1221        }
1222    }
1223
1224    private void newNodeInList(MimicNodeStore.NodeMemo nodeMemo) {
1225        // Filter for Tower LCC+Q
1226        NodeID node = nodeMemo.getNodeID();
1227        String id = node.toString();
1228        log.debug("node id: {}", id);
1229        if (!id.startsWith("02.01.57.4")) {
1230            return;
1231        }
1232
1233        int i = 0;
1234        if (_nodeModel.getIndexOf(nodeMemo.getNodeID()) >= 0) {
1235            // already exists. Do nothing.
1236            return;
1237        }
1238        NodeEntry e = new NodeEntry(nodeMemo);
1239
1240        while ((i < _nodeModel.getSize()) && (_nodeModel.getElementAt(i).compareTo(e) < 0)) {
1241            ++i;
1242        }
1243        _nodeModel.insertElementAt(e, i);
1244    }
1245
1246    private boolean isValidNodeVersionNumber(MimicNodeStore.NodeMemo nodeMemo) {
1247        SimpleNodeIdent ident = nodeMemo.getSimpleNodeIdent();
1248        String versionString = ident.getSoftwareVersion();
1249
1250        int version = 0;
1251        var match = PARSE_VERSION.matcher(versionString);
1252        if (match.find()) {
1253            var major = match.group(1);
1254            var minor = match.group(2);
1255            version = Integer.parseInt(major + minor);
1256        }
1257
1258        if (version < TOWER_LCC_Q_NODE_VERSION) {
1259            JmriJOptionPane.showMessageDialog(null,
1260                    Bundle.getMessage("MessageVersion",
1261                            nodeMemo.getNodeID(),
1262                            versionString,
1263                            TOWER_LCC_Q_NODE_VERSION_STRING),
1264                    Bundle.getMessage("TitleVersion"),
1265                    JmriJOptionPane.WARNING_MESSAGE);
1266            return false;
1267        }
1268
1269        return true;
1270    }
1271
1272    public class EntryListener implements PropertyChangeListener {
1273        @Override
1274        public void propertyChange(PropertyChangeEvent e) {
1275            String propertyName = e.getPropertyName();
1276            log.debug("EntryListener event = {}", propertyName);
1277
1278            if (propertyName.equals("PENDING_WRITE_COMPLETE")) {
1279                int currentLength = _storeQueueLength.decrementAndGet();
1280                log.debug("Listener: queue length = {}, source = {}", currentLength, e.getSource());
1281
1282                var entry = (ConfigRepresentation.CdiEntry) e.getSource();
1283                entry.removePropertyChangeListener(_entryListener);
1284
1285                if (currentLength < 1) {
1286                    log.debug("The queue is back to zero which implies the updates are done");
1287                    displayStoreDone();
1288                }
1289            }
1290
1291            if (_compileInProgress && propertyName.equals("UPDATE_ENTRY_DATA")) {
1292                // The refresh of the first syntax message has completed.
1293                var entry = (ConfigRepresentation.StringEntry) e.getSource();
1294                entry.removePropertyChangeListener(_entryListener);
1295                displayCompileMessage(entry.getValue());
1296            }
1297        }
1298    }
1299
1300    private void displayStoreDone() {
1301        _csvMessages.add(Bundle.getMessage("StoreDone"));
1302        var msgType = JmriJOptionPane.ERROR_MESSAGE;
1303        if (_csvMessages.size() == 1) {
1304            msgType = JmriJOptionPane.INFORMATION_MESSAGE;
1305        }
1306        JmriJOptionPane.showMessageDialog(this,
1307                String.join("\n", _csvMessages),
1308                Bundle.getMessage("TitleCdiStore"),
1309                msgType);
1310
1311        if (_compileNeeded) {
1312            log.debug("Display compile needed message");
1313
1314            String[] options = {Bundle.getMessage("EditorReboot"), Bundle.getMessage("CdiReboot")};
1315            int response = JmriJOptionPane.showOptionDialog(this,
1316                    Bundle.getMessage("MessageCdiReboot"),
1317                    Bundle.getMessage("TitleCdiReboot"),
1318                    JmriJOptionPane.YES_NO_OPTION,
1319                    JmriJOptionPane.QUESTION_MESSAGE,
1320                    null,
1321                    options,
1322                    options[0]);
1323
1324            if (response == JmriJOptionPane.YES_OPTION) {
1325                // Set the compile in process and request the reboot.  The completion will be
1326                // handed by the RebootListener.
1327                _compileInProgress = true;
1328                _cdi.getConnection().getDatagramService().
1329                        sendData(_cdi.getRemoteNodeID(), new int[] {0x20, 0xA9});
1330            }
1331        }
1332    }
1333
1334    /**
1335     * Get the first syntax message entry, add the entry listener and request a reload (refresh).
1336     * The EntryListener will handle the reload event.
1337     */
1338    private void getCompileMessage() {
1339            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(SYNTAX_MESSAGE);
1340            entry.addPropertyChangeListener(_entryListener);
1341            entry.reload();
1342    }
1343
1344    /**
1345     * Turn off the compile in progress and display the syntax message.
1346     * @param message The first syntax message.
1347     */
1348    private void displayCompileMessage(String message) {
1349        _compileInProgress = false;
1350        JmriJOptionPane.showMessageDialog(this,
1351                Bundle.getMessage("MessageCompile", message),
1352                Bundle.getMessage("TitleCompile"),
1353                JmriJOptionPane.INFORMATION_MESSAGE);
1354    }
1355
1356    // Notifies that the contents of a given entry have changed. This will delete and re-add the
1357    // entry to the model, forcing a refresh of the box.
1358    public void updateComboBoxModelEntry(NodeEntry nodeEntry) {
1359        int idx = _nodeModel.getIndexOf(nodeEntry.getNodeID());
1360        if (idx < 0) {
1361            return;
1362        }
1363        NodeEntry last = _nodeModel.getElementAt(idx);
1364        if (last != nodeEntry) {
1365            // not the same object -- we're talking about an abandoned entry.
1366            nodeEntry.dispose();
1367            return;
1368        }
1369        NodeEntry sel = (NodeEntry) _nodeModel.getSelectedItem();
1370        _nodeModel.removeElementAt(idx);
1371        _nodeModel.insertElementAt(nodeEntry, idx);
1372        _nodeModel.setSelectedItem(sel);
1373    }
1374
1375    protected static class NodeEntry implements Comparable<NodeEntry>, PropertyChangeListener {
1376        final MimicNodeStore.NodeMemo nodeMemo;
1377        String description = "";
1378
1379        NodeEntry(MimicNodeStore.NodeMemo memo) {
1380            this.nodeMemo = memo;
1381            memo.addPropertyChangeListener(this);
1382            updateDescription();
1383        }
1384
1385        /**
1386         * Constructor for prototype display value
1387         *
1388         * @param description prototype display value
1389         */
1390        public NodeEntry(String description) {
1391            this.nodeMemo = null;
1392            this.description = description;
1393        }
1394
1395        public NodeID getNodeID() {
1396            return nodeMemo.getNodeID();
1397        }
1398
1399        MimicNodeStore.NodeMemo getNodeMemo() {
1400            return nodeMemo;
1401        }
1402
1403        private void updateDescription() {
1404            SimpleNodeIdent ident = nodeMemo.getSimpleNodeIdent();
1405            StringBuilder sb = new StringBuilder();
1406            sb.append(nodeMemo.getNodeID().toString());
1407
1408            addToDescription(ident.getUserName(), sb);
1409            addToDescription(ident.getUserDesc(), sb);
1410            if (!ident.getMfgName().isEmpty() || !ident.getModelName().isEmpty()) {
1411                addToDescription(ident.getMfgName() + " " +ident.getModelName(), sb);
1412            }
1413            addToDescription(ident.getSoftwareVersion(), sb);
1414            String newDescription = sb.toString();
1415            if (!description.equals(newDescription)) {
1416                description = newDescription;
1417            }
1418        }
1419
1420        private void addToDescription(String s, StringBuilder sb) {
1421            if (!s.isEmpty()) {
1422                sb.append(" - ");
1423                sb.append(s);
1424            }
1425        }
1426
1427        private long reorder(long n) {
1428            return (n < 0) ? Long.MAX_VALUE - n : Long.MIN_VALUE + n;
1429        }
1430
1431        @Override
1432        public int compareTo(NodeEntry otherEntry) {
1433            long l1 = reorder(getNodeID().toLong());
1434            long l2 = reorder(otherEntry.getNodeID().toLong());
1435            return Long.compare(l1, l2);
1436        }
1437
1438        @Override
1439        public String toString() {
1440            return description;
1441        }
1442
1443        @Override
1444        @SuppressFBWarnings(value = "EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS",
1445                justification = "Purposefully attempting lookup using NodeID argument in model " +
1446                        "vector.")
1447        public boolean equals(Object o) {
1448            if (o instanceof NodeEntry) {
1449                return getNodeID().equals(((NodeEntry) o).getNodeID());
1450            }
1451            if (o instanceof NodeID) {
1452                return getNodeID().equals(o);
1453            }
1454            return false;
1455        }
1456
1457        @Override
1458        public int hashCode() {
1459            return getNodeID().hashCode();
1460        }
1461
1462        @Override
1463        public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
1464            //log.warning("Received model entry update for " + nodeMemo.getNodeID());
1465            if (propertyChangeEvent.getPropertyName().equals(UPDATE_PROP_SIMPLE_NODE_IDENT)) {
1466                updateDescription();
1467            }
1468        }
1469
1470        public void dispose() {
1471            //log.warning("dispose of " + nodeMemo.getNodeID().toString());
1472            nodeMemo.removePropertyChangeListener(this);
1473        }
1474    }
1475
1476    // --------------  load CDI data ---------
1477
1478    private void loadCdiData() {
1479        if (!replaceData()) {
1480            return;
1481        }
1482
1483        // Load data
1484        loadCdiInputs();
1485        loadCdiOutputs();
1486        loadCdiReceivers();
1487        loadCdiTransmitters();
1488        loadCdiGroups();
1489
1490        for (GroupRow row : _groupList) {
1491            decode(row);
1492        }
1493
1494        setDirty(false);
1495
1496        _groupTable.setRowSelectionInterval(0, 0);
1497
1498        _groupTable.repaint();
1499
1500        _exportButton.setEnabled(true);
1501        _refreshButton.setEnabled(true);
1502        _storeButton.setEnabled(true);
1503        _exportItem.setEnabled(true);
1504        _refreshItem.setEnabled(true);
1505        _storeItem.setEnabled(true);
1506
1507        if (_splitView) {
1508            _tableTabs.repaint();
1509        }
1510    }
1511
1512    private void pushedRefreshButton(ActionEvent e) {
1513        loadCdiData();
1514    }
1515
1516    private void loadCdiGroups() {
1517        for (int i = 0; i < 16; i++) {
1518            var groupRow = _groupList.get(i);
1519            groupRow.clearLogicList();
1520
1521            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(GROUP_NAME, i));
1522            groupRow.setName(entry.getValue());
1523            entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(GROUP_MULTI_LINE, i));
1524            groupRow.setMultiLine(entry.getValue());
1525        }
1526
1527        _groupTable.revalidate();
1528    }
1529
1530    private void loadCdiInputs() {
1531        for (int i = 0; i < 16; i++) {
1532            for (int j = 0; j < 8; j++) {
1533                var inputRow = _inputList.get((i * 8) + j);
1534
1535                var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(INPUT_NAME, i, j));
1536                inputRow.setName(entry.getValue());
1537                var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(INPUT_TRUE, i, j));
1538                inputRow.setEventTrue(event.getNumericalEventValue());
1539                event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(INPUT_FALSE, i, j));
1540                inputRow.setEventFalse(event.getNumericalEventValue());
1541            }
1542        }
1543        _inputTable.revalidate();
1544    }
1545
1546    private void loadCdiOutputs() {
1547        for (int i = 0; i < 16; i++) {
1548            for (int j = 0; j < 8; j++) {
1549                var outputRow = _outputList.get((i * 8) + j);
1550
1551                var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(OUTPUT_NAME, i, j));
1552                outputRow.setName(entry.getValue());
1553                var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(OUTPUT_TRUE, i, j));
1554                outputRow.setEventTrue(event.getNumericalEventValue());
1555                event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(OUTPUT_FALSE, i, j));
1556                outputRow.setEventFalse(event.getNumericalEventValue());
1557            }
1558        }
1559        _outputTable.revalidate();
1560    }
1561
1562    private void loadCdiReceivers() {
1563        for (int i = 0; i < 16; i++) {
1564            var receiverRow = _receiverList.get(i);
1565
1566            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(RECEIVER_NAME, i));
1567            receiverRow.setName(entry.getValue());
1568            var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(RECEIVER_EVENT, i));
1569            receiverRow.setEventId(event.getNumericalEventValue());
1570        }
1571        _receiverTable.revalidate();
1572    }
1573
1574    private void loadCdiTransmitters() {
1575        for (int i = 0; i < 16; i++) {
1576            var transmitterRow = _transmitterList.get(i);
1577
1578            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(TRANSMITTER_NAME, i));
1579            transmitterRow.setName(entry.getValue());
1580            var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(TRANSMITTER_EVENT, i));
1581            transmitterRow.setEventId(event.getNumericalEventValue());
1582        }
1583        _transmitterTable.revalidate();
1584    }
1585
1586    // --------------  store CDI data ---------
1587
1588    private void pushedStoreButton(ActionEvent e) {
1589        _csvMessages.clear();
1590        _compileNeeded = false;
1591        _storeQueueLength.set(0);
1592
1593        // Store CDI data
1594        storeInputs();
1595        storeOutputs();
1596        storeReceivers();
1597        storeTransmitters();
1598        storeGroups();
1599
1600        setDirty(false);
1601    }
1602
1603    private void storeGroups() {
1604        // store the group data
1605        int currentCount = 0;
1606
1607        for (int i = 0; i < 16; i++) {
1608            var row = _groupList.get(i);
1609
1610            // update the group line
1611            encode(row);
1612
1613            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(GROUP_NAME, i));
1614            if (!row.getName().equals(entry.getValue())) {
1615                entry.addPropertyChangeListener(_entryListener);
1616                entry.setValue(row.getName());
1617                currentCount = _storeQueueLength.incrementAndGet();
1618            }
1619
1620            entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(GROUP_MULTI_LINE, i));
1621            if (!row.getMultiLine().equals(entry.getValue())) {
1622                entry.addPropertyChangeListener(_entryListener);
1623                entry.setValue(row.getMultiLine());
1624                currentCount = _storeQueueLength.incrementAndGet();
1625                _compileNeeded = true;
1626            }
1627
1628            log.debug("Group: {}", row.getName());
1629            log.debug("Logic: {}", row.getMultiLine());
1630        }
1631        log.debug("storeGroups count = {}", currentCount);
1632    }
1633
1634    private void storeInputs() {
1635        int currentCount = 0;
1636
1637        for (int i = 0; i < 16; i++) {
1638            for (int j = 0; j < 8; j++) {
1639                var row = _inputList.get((i * 8) + j);
1640
1641                var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(INPUT_NAME, i, j));
1642                if (!row.getName().equals(entry.getValue())) {
1643                    entry.addPropertyChangeListener(_entryListener);
1644                    entry.setValue(row.getName());
1645                    currentCount = _storeQueueLength.incrementAndGet();
1646                }
1647
1648                var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(INPUT_TRUE, i, j));
1649                if (!row.getEventTrue().equals(event.getValue())) {
1650                    event.addPropertyChangeListener(_entryListener);
1651                    event.setValue(row.getEventTrue());
1652                    currentCount = _storeQueueLength.incrementAndGet();
1653                }
1654
1655                event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(INPUT_FALSE, i, j));
1656                if (!row.getEventFalse().equals(event.getValue())) {
1657                    event.addPropertyChangeListener(_entryListener);
1658                    event.setValue(row.getEventFalse());
1659                    currentCount = _storeQueueLength.incrementAndGet();
1660                }
1661            }
1662        }
1663        log.debug("storeInputs count = {}", currentCount);
1664    }
1665
1666    private void storeOutputs() {
1667        int currentCount = 0;
1668
1669        for (int i = 0; i < 16; i++) {
1670            for (int j = 0; j < 8; j++) {
1671                var row = _outputList.get((i * 8) + j);
1672
1673                var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(OUTPUT_NAME, i, j));
1674                if (!row.getName().equals(entry.getValue())) {
1675                    entry.addPropertyChangeListener(_entryListener);
1676                    entry.setValue(row.getName());
1677                    currentCount = _storeQueueLength.incrementAndGet();
1678                }
1679
1680                var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(OUTPUT_TRUE, i, j));
1681                if (!row.getEventTrue().equals(event.getValue())) {
1682                    event.addPropertyChangeListener(_entryListener);
1683                    event.setValue(row.getEventTrue());
1684                    currentCount = _storeQueueLength.incrementAndGet();
1685                }
1686
1687                event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(OUTPUT_FALSE, i, j));
1688                if (!row.getEventFalse().equals(event.getValue())) {
1689                    event.addPropertyChangeListener(_entryListener);
1690                    event.setValue(row.getEventFalse());
1691                    currentCount = _storeQueueLength.incrementAndGet();
1692                }
1693            }
1694        }
1695        log.debug("storeOutputs count = {}", currentCount);
1696    }
1697
1698    private void storeReceivers() {
1699        int currentCount = 0;
1700
1701        for (int i = 0; i < 16; i++) {
1702            var row = _receiverList.get(i);
1703
1704            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(RECEIVER_NAME, i));
1705            if (!row.getName().equals(entry.getValue())) {
1706                entry.addPropertyChangeListener(_entryListener);
1707                entry.setValue(row.getName());
1708                currentCount = _storeQueueLength.incrementAndGet();
1709            }
1710
1711            var event = (ConfigRepresentation.EventEntry) _cdi.getVariableForKey(String.format(RECEIVER_EVENT, i));
1712            if (!row.getEventId().equals(event.getValue())) {
1713                event.addPropertyChangeListener(_entryListener);
1714                event.setValue(row.getEventId());
1715                currentCount = _storeQueueLength.incrementAndGet();
1716            }
1717        }
1718        log.debug("storeReceivers count = {}", currentCount);
1719    }
1720
1721    private void storeTransmitters() {
1722        int currentCount = 0;
1723
1724        for (int i = 0; i < 16; i++) {
1725            var row = _transmitterList.get(i);
1726
1727            var entry = (ConfigRepresentation.StringEntry) _cdi.getVariableForKey(String.format(TRANSMITTER_NAME, i));
1728            if (!row.getName().equals(entry.getValue())) {
1729                entry.addPropertyChangeListener(_entryListener);
1730                entry.setValue(row.getName());
1731                currentCount = _storeQueueLength.incrementAndGet();
1732            }
1733        }
1734        log.debug("storeTransmitters count = {}", currentCount);
1735    }
1736
1737    // --------------  Backup Import ---------
1738
1739    private void loadBackupData(ActionEvent m) {
1740        if (!replaceData()) {
1741            return;
1742        }
1743
1744        var fileChooser = new JmriJFileChooser(FileUtil.getUserFilesPath());
1745        fileChooser.setApproveButtonText(Bundle.getMessage("LoadCdiButton"));
1746        fileChooser.setDialogTitle(Bundle.getMessage("LoadCdiTitle"));
1747        var filter = new FileNameExtensionFilter(Bundle.getMessage("LoadCdiFilter"), "txt");
1748        fileChooser.addChoosableFileFilter(filter);
1749        fileChooser.setFileFilter(filter);
1750
1751        int response = fileChooser.showOpenDialog(this);
1752        if (response == JFileChooser.CANCEL_OPTION) {
1753            return;
1754        }
1755
1756        List<String> lines = null;
1757        try {
1758            lines = Files.readAllLines(Paths.get(fileChooser.getSelectedFile().getAbsolutePath()));
1759        } catch (IOException e) {
1760            log.error("Failed to load file.", e);
1761            return;
1762        }
1763
1764        for (int i = 0; i < lines.size(); i++) {
1765            if (lines.get(i).startsWith("Logic Inputs.Group")) {
1766                loadBackupInputs(i, lines);
1767                i += 128 * 3;
1768            }
1769
1770            if (lines.get(i).startsWith("Logic Outputs.Group")) {
1771                loadBackupOutputs(i, lines);
1772                i += 128 * 3;
1773            }
1774            if (lines.get(i).startsWith("Track Receivers")) {
1775                loadBackupReceivers(i, lines);
1776                i += 16 * 2;
1777            }
1778            if (lines.get(i).startsWith("Track Transmitters")) {
1779                loadBackupTransmitters(i, lines);
1780                i += 16 * 2;
1781            }
1782            if (lines.get(i).startsWith("Conditionals.Logic")) {
1783                loadBackupGroups(i, lines);
1784                i += 16 * 2;
1785            }
1786        }
1787
1788        for (GroupRow row : _groupList) {
1789            decode(row);
1790        }
1791
1792        setDirty(false);
1793        _groupTable.setRowSelectionInterval(0, 0);
1794        _groupTable.repaint();
1795
1796        _exportButton.setEnabled(true);
1797        _exportItem.setEnabled(true);
1798
1799        if (_splitView) {
1800            _tableTabs.repaint();
1801        }
1802    }
1803
1804    private String getLineValue(String line) {
1805        if (line.endsWith("=")) {
1806            return "";
1807        }
1808        int index = line.indexOf("=");
1809        var newLine = line.substring(index + 1);
1810        newLine = Util.unescapeString(newLine);
1811        return newLine;
1812    }
1813
1814    /**
1815     * The event id will be a dotted-hex or an 'event name'.  Event names need to be converted to
1816     * the actual dotted-hex value.  If the name no longer exists in the name store, a zeros
1817     * event is created as 00.00.00.00.00.AA.BB.CC.  AA will the hex value of one of IQYZ.  BB and
1818     * CC are hex values of the group and item numbers.
1819     * @param event The dotted-hex event id or event name
1820     * @param iqyz The character for the table.
1821     * @param row The row number.
1822     * @return a dotted-hex event id string.
1823     */
1824    private String getLoadEventID(String event, char iqyz, int row) {
1825        if (isEventValid(event)) {
1826            return event;
1827        }
1828
1829        try {
1830            EventID eventID = _nameStore.getEventID(event);
1831            return eventID.toShortString();
1832        }
1833        catch (NumberFormatException ex) {
1834            log.error("STL Editor getLoadEventID event failed for event name {}", event);
1835        }
1836
1837        // Create zeros event dotted-hex string
1838        var group = row;
1839        var item = 0;
1840        if (iqyz == 'I' || iqyz == 'Q') {
1841            group = row / 8;
1842            item = row % 8;
1843        }
1844
1845        var sb = new StringBuilder("00.00.00.00.00.");
1846        sb.append(StringUtil.twoHexFromInt(iqyz));
1847        sb.append(".");
1848        sb.append(StringUtil.twoHexFromInt(group));
1849        sb.append(".");
1850        sb.append(StringUtil.twoHexFromInt(item));
1851        var zeroEvent = sb.toString();
1852
1853        JmriJOptionPane.showMessageDialog(null,
1854                Bundle.getMessage("MessageEvent", event, zeroEvent, iqyz),
1855                Bundle.getMessage("TitleEvent"),
1856                JmriJOptionPane.ERROR_MESSAGE);
1857
1858        return zeroEvent;
1859    }
1860
1861    private void loadBackupInputs(int index, List<String> lines) {
1862        for (int i = 0; i < 128; i++) {
1863            var inputRow = _inputList.get(i);
1864
1865            inputRow.setName(getLineValue(lines.get(index)));
1866            var trueName = getLineValue(lines.get(index + 1));
1867            inputRow.setEventTrue(getLoadEventID(trueName, 'I', i));
1868            var falseName = getLineValue(lines.get(index + 2));
1869            inputRow.setEventFalse(getLoadEventID(falseName, 'I',i));
1870
1871            index += 3;
1872        }
1873
1874        _inputTable.revalidate();
1875    }
1876
1877    private void loadBackupOutputs(int index, List<String> lines) {
1878        for (int i = 0; i < 128; i++) {
1879            var outputRow = _outputList.get(i);
1880
1881            outputRow.setName(getLineValue(lines.get(index)));
1882            var trueName = getLineValue(lines.get(index + 1));
1883            outputRow.setEventTrue(getLoadEventID(trueName, 'Q', i));
1884            var falseName = getLineValue(lines.get(index + 2));
1885            outputRow.setEventFalse(getLoadEventID(falseName, 'Q', i));
1886
1887            index += 3;
1888        }
1889
1890        _outputTable.revalidate();
1891    }
1892
1893    private void loadBackupReceivers(int index, List<String> lines) {
1894        for (int i = 0; i < 16; i++) {
1895            var receiverRow = _receiverList.get(i);
1896
1897            receiverRow.setName(getLineValue(lines.get(index)));
1898            var event = getLineValue(lines.get(index + 1));
1899            receiverRow.setEventId(getLoadEventID(event, 'Y', i));
1900
1901            index += 2;
1902        }
1903
1904        _receiverTable.revalidate();
1905    }
1906
1907    private void loadBackupTransmitters(int index, List<String> lines) {
1908        for (int i = 0; i < 16; i++) {
1909            var transmitterRow = _transmitterList.get(i);
1910
1911            transmitterRow.setName(getLineValue(lines.get(index)));
1912            var event = getLineValue(lines.get(index + 1));
1913            transmitterRow.setEventId(getLoadEventID(event, 'Z', i));
1914
1915            index += 2;
1916        }
1917
1918        _transmitterTable.revalidate();
1919    }
1920
1921    private void loadBackupGroups(int index, List<String> lines) {
1922        for (int i = 0; i < 16; i++) {
1923            var groupRow = _groupList.get(i);
1924            groupRow.clearLogicList();
1925
1926            groupRow.setName(getLineValue(lines.get(index)));
1927            groupRow.setMultiLine(getLineValue(lines.get(index + 1)));
1928            index += 2;
1929        }
1930
1931        _groupTable.revalidate();
1932        _logicTable.revalidate();
1933    }
1934
1935    // --------------  CSV Import ---------
1936
1937    private void pushedImportButton(ActionEvent e) {
1938        if (!replaceData()) {
1939            return;
1940        }
1941
1942        if (!setCsvDirectoryPath(true)) {
1943            return;
1944        }
1945
1946        _csvMessages.clear();
1947        importCsvData();
1948        setDirty(false);
1949
1950        _exportButton.setEnabled(true);
1951        _exportItem.setEnabled(true);
1952
1953        if (!_csvMessages.isEmpty()) {
1954            JmriJOptionPane.showMessageDialog(this,
1955                    String.join("\n", _csvMessages),
1956                    Bundle.getMessage("TitleCsvImport"),
1957                    JmriJOptionPane.ERROR_MESSAGE);
1958        }
1959    }
1960
1961    private void importCsvData() {
1962        importGroupLogic();
1963        importInputs();
1964        importOutputs();
1965        importReceivers();
1966        importTransmitters();
1967
1968        _groupTable.setRowSelectionInterval(0, 0);
1969
1970        _groupTable.repaint();
1971
1972        if (_splitView) {
1973            _tableTabs.repaint();
1974        }
1975    }
1976
1977    /**
1978     * The group logic file contains 16 group rows and a variable number of logic rows for each group.
1979     * The exported CSV file has one field for the group rows and 5 fields for the logic rows.
1980     * If the CSV file has been modified by a spreadsheet, the group rows will now have 5 fields.
1981     */
1982    private void importGroupLogic() {
1983        List<CSVRecord> records = getCsvRecords("group_logic.csv");
1984        if (records.isEmpty()) {
1985            return;
1986        }
1987
1988        var skipHeader = true;
1989        int groupNumber = -1;
1990        for (CSVRecord record : records) {
1991            if (skipHeader) {
1992                skipHeader = false;
1993                continue;
1994            }
1995
1996            List<String> values = new ArrayList<>();
1997            record.forEach(values::add);
1998
1999            if (values.size() == 1 || (values.size() == 5 &&
2000                    values.get(1).isEmpty() &&
2001                    values.get(2).isEmpty() &&
2002                    values.get(3).isEmpty() &&
2003                    values.get(4).isEmpty())) {
2004                // Create Group
2005                groupNumber++;
2006                var groupRow = _groupList.get(groupNumber);
2007                groupRow.setName(values.get(0));
2008                groupRow.setMultiLine("");
2009                groupRow.clearLogicList();
2010            } else if (values.size() == 5) {
2011                var oper = getEnum(values.get(2));
2012                var logicRow = new LogicRow(values.get(1), oper, values.get(3), values.get(4));
2013                _groupList.get(groupNumber).getLogicList().add(logicRow);
2014            } else {
2015                _csvMessages.add(Bundle.getMessage("ImportGroupError", record.toString()));
2016            }
2017        }
2018
2019        _groupTable.revalidate();
2020        _logicTable.revalidate();
2021    }
2022
2023    private void importInputs() {
2024        List<CSVRecord> records = getCsvRecords("inputs.csv");
2025        if (records.isEmpty()) {
2026            return;
2027        }
2028
2029        for (int i = 0; i < 129; i++) {
2030            if (i == 0) {
2031                continue;
2032            }
2033
2034            var record = records.get(i);
2035            List<String> values = new ArrayList<>();
2036            record.forEach(values::add);
2037
2038            if (values.size() == 4) {
2039                var inputRow = _inputList.get(i - 1);
2040                inputRow.setName(values.get(1));
2041                inputRow.setEventTrue(values.get(2));
2042                inputRow.setEventFalse(values.get(3));
2043            } else {
2044                _csvMessages.add(Bundle.getMessage("ImportInputError", record.toString()));
2045            }
2046        }
2047
2048        _inputTable.revalidate();
2049    }
2050
2051    private void importOutputs() {
2052        List<CSVRecord> records = getCsvRecords("outputs.csv");
2053        if (records.isEmpty()) {
2054            return;
2055        }
2056
2057        for (int i = 0; i < 129; i++) {
2058            if (i == 0) {
2059                continue;
2060            }
2061
2062            var record = records.get(i);
2063            List<String> values = new ArrayList<>();
2064            record.forEach(values::add);
2065
2066            if (values.size() == 4) {
2067                var outputRow = _outputList.get(i - 1);
2068                outputRow.setName(values.get(1));
2069                outputRow.setEventTrue(values.get(2));
2070                outputRow.setEventFalse(values.get(3));
2071            } else {
2072                _csvMessages.add(Bundle.getMessage("ImportOuputError", record.toString()));
2073            }
2074        }
2075
2076        _outputTable.revalidate();
2077    }
2078
2079    private void importReceivers() {
2080        List<CSVRecord> records = getCsvRecords("receivers.csv");
2081        if (records.isEmpty()) {
2082            return;
2083        }
2084
2085        for (int i = 0; i < 17; i++) {
2086            if (i == 0) {
2087                continue;
2088            }
2089
2090            var record = records.get(i);
2091            List<String> values = new ArrayList<>();
2092            record.forEach(values::add);
2093
2094            if (values.size() == 3) {
2095                var receiverRow = _receiverList.get(i - 1);
2096                receiverRow.setName(values.get(1));
2097                receiverRow.setEventId(values.get(2));
2098            } else {
2099                _csvMessages.add(Bundle.getMessage("ImportReceiverError", record.toString()));
2100            }
2101        }
2102
2103        _receiverTable.revalidate();
2104    }
2105
2106    private void importTransmitters() {
2107        List<CSVRecord> records = getCsvRecords("transmitters.csv");
2108        if (records.isEmpty()) {
2109            return;
2110        }
2111
2112        for (int i = 0; i < 17; i++) {
2113            if (i == 0) {
2114                continue;
2115            }
2116
2117            var record = records.get(i);
2118            List<String> values = new ArrayList<>();
2119            record.forEach(values::add);
2120
2121            if (values.size() == 3) {
2122                var transmitterRow = _transmitterList.get(i - 1);
2123                transmitterRow.setName(values.get(1));
2124                transmitterRow.setEventId(values.get(2));
2125            } else {
2126                _csvMessages.add(Bundle.getMessage("ImportTransmitterError", record.toString()));
2127            }
2128        }
2129
2130        _transmitterTable.revalidate();
2131    }
2132
2133    private List<CSVRecord> getCsvRecords(String fileName) {
2134        var recordList = new ArrayList<CSVRecord>();
2135        FileReader fileReader;
2136        try {
2137            fileReader = new FileReader(_csvDirectoryPath + fileName);
2138        } catch (FileNotFoundException ex) {
2139            _csvMessages.add(Bundle.getMessage("ImportFileNotFound", fileName));
2140            return recordList;
2141        }
2142
2143        BufferedReader bufferedReader;
2144        CSVParser csvFile;
2145
2146        try {
2147            bufferedReader = new BufferedReader(fileReader);
2148            csvFile = new CSVParser(bufferedReader, CSVFormat.DEFAULT);
2149            recordList.addAll(csvFile.getRecords());
2150            csvFile.close();
2151            bufferedReader.close();
2152            fileReader.close();
2153        } catch (IOException iox) {
2154            _csvMessages.add(Bundle.getMessage("ImportFileIOError", iox.getMessage(), fileName));
2155        }
2156
2157        return recordList;
2158    }
2159
2160    // --------------  CSV Export ---------
2161
2162    private void pushedExportButton(ActionEvent e) {
2163        if (!setCsvDirectoryPath(false)) {
2164            return;
2165        }
2166
2167        _csvMessages.clear();
2168        exportCsvData();
2169        setDirty(false);
2170
2171        _csvMessages.add(Bundle.getMessage("ExportDone"));
2172        var msgType = JmriJOptionPane.ERROR_MESSAGE;
2173        if (_csvMessages.size() == 1) {
2174            msgType = JmriJOptionPane.INFORMATION_MESSAGE;
2175        }
2176        JmriJOptionPane.showMessageDialog(this,
2177                String.join("\n", _csvMessages),
2178                Bundle.getMessage("TitleCsvExport"),
2179                msgType);
2180    }
2181
2182    private void exportCsvData() {
2183        try {
2184            exportGroupLogic();
2185            exportInputs();
2186            exportOutputs();
2187            exportReceivers();
2188            exportTransmitters();
2189        } catch (IOException ex) {
2190            _csvMessages.add(Bundle.getMessage("ExportIOError", ex.getMessage()));
2191        }
2192
2193    }
2194
2195    private void exportGroupLogic() throws IOException {
2196        var fileWriter = new FileWriter(_csvDirectoryPath + "group_logic.csv");
2197        var bufferedWriter = new BufferedWriter(fileWriter);
2198        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
2199
2200        csvFile.printRecord(Bundle.getMessage("GroupName"), Bundle.getMessage("ColumnLabel"),
2201                 Bundle.getMessage("ColumnOper"), Bundle.getMessage("ColumnName"), Bundle.getMessage("ColumnComment"));
2202
2203        for (int i = 0; i < 16; i++) {
2204            var row = _groupList.get(i);
2205            var groupName = row.getName();
2206            csvFile.printRecord(groupName);
2207            var logicRow = row.getLogicList();
2208            for (LogicRow logic : logicRow) {
2209                var operName = logic.getOperName();
2210
2211                // skip empty logic rows since they look like an empty group row
2212                if (logic.getLabel().isEmpty() &&
2213                        operName.isEmpty() &&
2214                        logic.getName().isEmpty() &&
2215                        logic.getComment().isEmpty()) {
2216                    log.info("skip empty row");
2217                    continue;
2218                }
2219
2220                csvFile.printRecord("", logic.getLabel(), operName, logic.getName(), logic.getComment());
2221            }
2222        }
2223
2224        // Flush the write buffer and close the file
2225        csvFile.flush();
2226        csvFile.close();
2227    }
2228
2229    private void exportInputs() throws IOException {
2230        var fileWriter = new FileWriter(_csvDirectoryPath + "inputs.csv");
2231        var bufferedWriter = new BufferedWriter(fileWriter);
2232        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
2233
2234        csvFile.printRecord(Bundle.getMessage("ColumnInput"), Bundle.getMessage("ColumnName"),
2235                 Bundle.getMessage("ColumnTrue"), Bundle.getMessage("ColumnFalse"));
2236
2237        for (int i = 0; i < 16; i++) {
2238            for (int j = 0; j < 8; j++) {
2239                var variable = "I" + i + "." + j;
2240                var row = _inputList.get((i * 8) + j);
2241                csvFile.printRecord(variable, row.getName(), row.getEventTrue(), row.getEventFalse());
2242            }
2243        }
2244
2245        // Flush the write buffer and close the file
2246        csvFile.flush();
2247        csvFile.close();
2248    }
2249
2250    private void exportOutputs() throws IOException {
2251        var fileWriter = new FileWriter(_csvDirectoryPath + "outputs.csv");
2252        var bufferedWriter = new BufferedWriter(fileWriter);
2253        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
2254
2255        csvFile.printRecord(Bundle.getMessage("ColumnOutput"), Bundle.getMessage("ColumnName"),
2256                 Bundle.getMessage("ColumnTrue"), Bundle.getMessage("ColumnFalse"));
2257
2258        for (int i = 0; i < 16; i++) {
2259            for (int j = 0; j < 8; j++) {
2260                var variable = "Q" + i + "." + j;
2261                var row = _outputList.get((i * 8) + j);
2262                csvFile.printRecord(variable, row.getName(), row.getEventTrue(), row.getEventFalse());
2263            }
2264        }
2265
2266        // Flush the write buffer and close the file
2267        csvFile.flush();
2268        csvFile.close();
2269    }
2270
2271    private void exportReceivers() throws IOException {
2272        var fileWriter = new FileWriter(_csvDirectoryPath + "receivers.csv");
2273        var bufferedWriter = new BufferedWriter(fileWriter);
2274        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
2275
2276        csvFile.printRecord(Bundle.getMessage("ColumnCircuit"), Bundle.getMessage("ColumnName"),
2277                 Bundle.getMessage("ColumnEventID"));
2278
2279        for (int i = 0; i < 16; i++) {
2280            var variable = "Y" + i;
2281            var row = _receiverList.get(i);
2282            csvFile.printRecord(variable, row.getName(), row.getEventId());
2283        }
2284
2285        // Flush the write buffer and close the file
2286        csvFile.flush();
2287        csvFile.close();
2288    }
2289
2290    private void exportTransmitters() throws IOException {
2291        var fileWriter = new FileWriter(_csvDirectoryPath + "transmitters.csv");
2292        var bufferedWriter = new BufferedWriter(fileWriter);
2293        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
2294
2295        csvFile.printRecord(Bundle.getMessage("ColumnCircuit"), Bundle.getMessage("ColumnName"),
2296                 Bundle.getMessage("ColumnEventID"));
2297
2298        for (int i = 0; i < 16; i++) {
2299            var variable = "Z" + i;
2300            var row = _transmitterList.get(i);
2301            csvFile.printRecord(variable, row.getName(), row.getEventId());
2302        }
2303
2304        // Flush the write buffer and close the file
2305        csvFile.flush();
2306        csvFile.close();
2307    }
2308
2309    /**
2310     * Select the directory that will be used for the CSV file set.
2311     * @param isOpen - True for CSV Import and false for CSV Export.
2312     * @return true if a directory was selected.
2313     */
2314    private boolean setCsvDirectoryPath(boolean isOpen) {
2315        var directoryChooser = new JmriJFileChooser(FileUtil.getUserFilesPath());
2316        directoryChooser.setApproveButtonText(Bundle.getMessage("SelectCsvButton"));
2317        directoryChooser.setDialogTitle(Bundle.getMessage("SelectCsvTitle"));
2318        directoryChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
2319
2320        int response = 0;
2321        if (isOpen) {
2322            response = directoryChooser.showOpenDialog(this);
2323        } else {
2324            response = directoryChooser.showSaveDialog(this);
2325        }
2326        if (response != JFileChooser.APPROVE_OPTION) {
2327            return false;
2328        }
2329        _csvDirectoryPath = directoryChooser.getSelectedFile().getAbsolutePath() + FileUtil.SEPARATOR;
2330
2331        return true;
2332    }
2333
2334    // --------------  Data Utilities ---------
2335
2336    private void setDirty(boolean dirty) {
2337        _dirty = dirty;
2338    }
2339
2340    private boolean isDirty() {
2341        return _dirty;
2342    }
2343
2344    private boolean replaceData() {
2345        if (isDirty()) {
2346            int response = JmriJOptionPane.showConfirmDialog(this,
2347                    Bundle.getMessage("MessageRevert"),
2348                    Bundle.getMessage("TitleRevert"),
2349                    JmriJOptionPane.YES_NO_OPTION);
2350            if (response != JmriJOptionPane.YES_OPTION) {
2351                return false;
2352            }
2353        }
2354        return true;
2355    }
2356
2357    private void warningDialog(String title, String message) {
2358        JmriJOptionPane.showMessageDialog(this,
2359            message,
2360            title,
2361            JmriJOptionPane.WARNING_MESSAGE);
2362    }
2363
2364    // --------------  Data validation ---------
2365
2366    static boolean isLabelValid(String label) {
2367        if (label.isEmpty()) {
2368            return true;
2369        }
2370
2371        var match = PARSE_LABEL.matcher(label);
2372        if (match.find()) {
2373            return true;
2374        }
2375
2376        JmriJOptionPane.showMessageDialog(null,
2377                Bundle.getMessage("MessageLabel", label),
2378                Bundle.getMessage("TitleLabel"),
2379                JmriJOptionPane.ERROR_MESSAGE);
2380        return false;
2381    }
2382
2383    static boolean isEventValid(String event) {
2384        var valid = true;
2385
2386        if (event.isEmpty()) {
2387            return valid;
2388        }
2389
2390        var hexPairs = event.split("\\.");
2391        if (hexPairs.length != 8) {
2392            valid = false;
2393        } else {
2394            for (int i = 0; i < 8; i++) {
2395                var match = PARSE_HEXPAIR.matcher(hexPairs[i]);
2396                if (!match.find()) {
2397                    valid = false;
2398                    break;
2399                }
2400            }
2401        }
2402
2403        return valid;
2404    }
2405
2406    // --------------  table lists ---------
2407
2408    /**
2409     * The Group row contains the name and the raw data for one of the 16 groups.
2410     * It also contains the decoded logic for the group in the logic list.
2411     */
2412    static class GroupRow {
2413        String _name;
2414        String _multiLine = "";
2415        List<LogicRow> _logicList = new ArrayList<>();
2416
2417
2418        GroupRow(String name) {
2419            _name = name;
2420        }
2421
2422        String getName() {
2423            return _name;
2424        }
2425
2426        void setName(String newName) {
2427            _name = newName;
2428        }
2429
2430        List<LogicRow> getLogicList() {
2431            return _logicList;
2432        }
2433
2434        void setLogicList(List<LogicRow> logicList) {
2435            _logicList.clear();
2436            _logicList.addAll(logicList);
2437        }
2438
2439        void clearLogicList() {
2440            _logicList.clear();
2441        }
2442
2443        String getMultiLine() {
2444            return _multiLine;
2445        }
2446
2447        void setMultiLine(String newMultiLine) {
2448            _multiLine = newMultiLine.strip();
2449        }
2450
2451        String getSize() {
2452            int size = (_multiLine.length() * 100) / 255;
2453            return String.valueOf(size) + "%";
2454        }
2455    }
2456
2457    /**
2458     * The definition of a logic row
2459     */
2460    static class LogicRow {
2461        String _label;
2462        Operator _oper;
2463        String _name;
2464        String _comment;
2465
2466        LogicRow(String label, Operator oper, String name, String comment) {
2467            _label = label;
2468            _oper = oper;
2469            _name = name;
2470            _comment = comment;
2471        }
2472
2473        String getLabel() {
2474            return _label;
2475        }
2476
2477        void setLabel(String newLabel) {
2478            var label = newLabel.trim();
2479            if (isLabelValid(label)) {
2480                _label = label;
2481            }
2482        }
2483
2484        Operator getOper() {
2485            return _oper;
2486        }
2487
2488        String getOperName() {
2489            if (_oper == null) {
2490                return "";
2491            }
2492
2493            String operName = _oper.name();
2494
2495            // Fix special enums
2496            if (operName.equals("Cp")) {
2497                operName = ")";
2498            } else if (operName.equals("EQ")) {
2499                operName = "=";
2500            } else if (operName.contains("p")) {
2501                operName = operName.replace("p", "(");
2502            }
2503
2504            return operName;
2505        }
2506
2507        void setOper(Operator newOper) {
2508            _oper = newOper;
2509        }
2510
2511        String getName() {
2512            return _name;
2513        }
2514
2515        void setName(String newName) {
2516            _name = newName.trim();
2517        }
2518
2519        String getComment() {
2520            return _comment;
2521        }
2522
2523        void setComment(String newComment) {
2524            _comment = newComment;
2525        }
2526    }
2527
2528    /**
2529     * The name and assigned true and false events for an Input.
2530     */
2531    static class InputRow {
2532        String _name;
2533        String _eventTrue;
2534        String _eventFalse;
2535
2536        InputRow(String name, String eventTrue, String eventFalse) {
2537            _name = name;
2538            _eventTrue = eventTrue;
2539            _eventFalse = eventFalse;
2540        }
2541
2542        String getName() {
2543            return _name;
2544        }
2545
2546        void setName(String newName) {
2547            _name = newName.trim();
2548        }
2549
2550        String getEventTrue() {
2551            if (_eventTrue.length() == 0) return "00.00.00.00.00.00.00.00";
2552            return _eventTrue;
2553        }
2554
2555        void setEventTrue(String newEventTrue) {
2556            _eventTrue = newEventTrue.trim();
2557        }
2558
2559        String getEventFalse() {
2560            if (_eventFalse.length() == 0) return "00.00.00.00.00.00.00.00";
2561            return _eventFalse;
2562        }
2563
2564        void setEventFalse(String newEventFalse) {
2565            _eventFalse = newEventFalse.trim();
2566        }
2567    }
2568
2569    /**
2570     * The name and assigned true and false events for an Output.
2571     */
2572    static class OutputRow {
2573        String _name;
2574        String _eventTrue;
2575        String _eventFalse;
2576
2577        OutputRow(String name, String eventTrue, String eventFalse) {
2578            _name = name;
2579            _eventTrue = eventTrue;
2580            _eventFalse = eventFalse;
2581        }
2582
2583        String getName() {
2584            return _name;
2585        }
2586
2587        void setName(String newName) {
2588            _name = newName.trim();
2589        }
2590
2591        String getEventTrue() {
2592            if (_eventTrue.length() == 0) return "00.00.00.00.00.00.00.00";
2593            return _eventTrue;
2594        }
2595
2596        void setEventTrue(String newEventTrue) {
2597            _eventTrue = newEventTrue.trim();
2598        }
2599
2600        String getEventFalse() {
2601            if (_eventFalse.length() == 0) return "00.00.00.00.00.00.00.00";
2602            return _eventFalse;
2603        }
2604
2605        void setEventFalse(String newEventFalse) {
2606            _eventFalse = newEventFalse.trim();
2607        }
2608    }
2609
2610    /**
2611     * The name and assigned event id for a circuit receiver.
2612     */
2613    static class ReceiverRow {
2614        String _name;
2615        String _eventid;
2616
2617        ReceiverRow(String name, String eventid) {
2618            _name = name;
2619            _eventid = eventid;
2620        }
2621
2622        String getName() {
2623            return _name;
2624        }
2625
2626        void setName(String newName) {
2627            _name = newName.trim();
2628        }
2629
2630        String getEventId() {
2631            if (_eventid.length() == 0) return "00.00.00.00.00.00.00.00";
2632            return _eventid;
2633        }
2634
2635        void setEventId(String newEventid) {
2636            _eventid = newEventid.trim();
2637        }
2638    }
2639
2640    /**
2641     * The name and assigned event id for a circuit transmitter.
2642     */
2643    static class TransmitterRow {
2644        String _name;
2645        String _eventid;
2646
2647        TransmitterRow(String name, String eventid) {
2648            _name = name;
2649            _eventid = eventid;
2650        }
2651
2652        String getName() {
2653            return _name;
2654        }
2655
2656        void setName(String newName) {
2657            _name = newName.trim();
2658        }
2659
2660        String getEventId() {
2661            if (_eventid.length() == 0) return "00.00.00.00.00.00.00.00";
2662            return _eventid;
2663        }
2664
2665        void setEventId(String newEventid) {
2666            _eventid = newEventid.trim();
2667        }
2668    }
2669
2670    // --------------  table models ---------
2671
2672    /**
2673     * The table input can be either a valid dotted-hex string or an "event name". If the input is
2674     * an event name, the name has to be converted to a dotted-hex string.  Creating a new event
2675     * name is not supported.
2676     * @param event The dotted-hex or event name string.
2677     * @return the dotted-hex string or null if the event name is not in the name store.
2678     */
2679    private String getTableInputEventID(String event) {
2680        if (isEventValid(event)) {
2681            return event;
2682        }
2683
2684        try {
2685            EventID eventID = _nameStore.getEventID(event);
2686            return eventID.toShortString();
2687        }
2688        catch (NumberFormatException num) {
2689            log.error("STL Editor getTableInputEventID event failed for event name {} (NumberFormatException)", event);
2690        } catch (IllegalArgumentException arg) {
2691            log.error("STL Editor getTableInputEventID event failed for event name {} (IllegalArgumentException)", event);
2692        }
2693
2694        JmriJOptionPane.showMessageDialog(null,
2695                Bundle.getMessage("MessageEventTable", event),
2696                Bundle.getMessage("TitleEventTable"),
2697                JmriJOptionPane.ERROR_MESSAGE);
2698
2699        return null;
2700
2701    }
2702
2703    /**
2704     * TableModel for Group table entries.
2705     */
2706    class GroupModel extends AbstractTableModel {
2707
2708        GroupModel() {
2709        }
2710
2711        public static final int ROW_COLUMN = 0;
2712        public static final int NAME_COLUMN = 1;
2713
2714        @Override
2715        public int getRowCount() {
2716            return _groupList.size();
2717        }
2718
2719        @Override
2720        public int getColumnCount() {
2721            return 2;
2722        }
2723
2724        @Override
2725        public Class<?> getColumnClass(int c) {
2726            return String.class;
2727        }
2728
2729        @Override
2730        public String getColumnName(int col) {
2731            switch (col) {
2732                case ROW_COLUMN:
2733                    return "";
2734                case NAME_COLUMN:
2735                    return Bundle.getMessage("ColumnName");
2736                default:
2737                    return "unknown";  // NOI18N
2738            }
2739        }
2740
2741        @Override
2742        public Object getValueAt(int r, int c) {
2743            switch (c) {
2744                case ROW_COLUMN:
2745                    return r + 1;
2746                case NAME_COLUMN:
2747                    return _groupList.get(r).getName();
2748                default:
2749                    return null;
2750            }
2751        }
2752
2753        @Override
2754        public void setValueAt(Object type, int r, int c) {
2755            switch (c) {
2756                case NAME_COLUMN:
2757                    _groupList.get(r).setName((String) type);
2758                    setDirty(true);
2759                    break;
2760                default:
2761                    break;
2762            }
2763        }
2764
2765        @Override
2766        public boolean isCellEditable(int r, int c) {
2767            return (c == NAME_COLUMN);
2768        }
2769
2770        public int getPreferredWidth(int col) {
2771            switch (col) {
2772                case ROW_COLUMN:
2773                    return new JTextField(4).getPreferredSize().width;
2774                case NAME_COLUMN:
2775                    return new JTextField(20).getPreferredSize().width;
2776                default:
2777                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2778                    return new JTextField(8).getPreferredSize().width;
2779            }
2780        }
2781    }
2782
2783    /**
2784     * TableModel for STL table entries.
2785     */
2786    class LogicModel extends AbstractTableModel {
2787
2788        LogicModel() {
2789        }
2790
2791        public static final int LABEL_COLUMN = 0;
2792        public static final int OPER_COLUMN = 1;
2793        public static final int NAME_COLUMN = 2;
2794        public static final int COMMENT_COLUMN = 3;
2795
2796        @Override
2797        public int getRowCount() {
2798            var logicList = _groupList.get(_groupRow).getLogicList();
2799            return logicList.size();
2800        }
2801
2802        @Override
2803        public int getColumnCount() {
2804            return 4;
2805        }
2806
2807        @Override
2808        public Class<?> getColumnClass(int c) {
2809            if (c == OPER_COLUMN) return JComboBox.class;
2810            return String.class;
2811        }
2812
2813        @Override
2814        public String getColumnName(int col) {
2815            switch (col) {
2816                case LABEL_COLUMN:
2817                    return Bundle.getMessage("ColumnLabel");  // NOI18N
2818                case OPER_COLUMN:
2819                    return Bundle.getMessage("ColumnOper");  // NOI18N
2820                case NAME_COLUMN:
2821                    return Bundle.getMessage("ColumnName");  // NOI18N
2822                case COMMENT_COLUMN:
2823                    return Bundle.getMessage("ColumnComment");  // NOI18N
2824                default:
2825                    return "unknown";  // NOI18N
2826            }
2827        }
2828
2829        @Override
2830        public Object getValueAt(int r, int c) {
2831            var logicList = _groupList.get(_groupRow).getLogicList();
2832            switch (c) {
2833                case LABEL_COLUMN:
2834                    return logicList.get(r).getLabel();
2835                case OPER_COLUMN:
2836                    return logicList.get(r).getOper();
2837                case NAME_COLUMN:
2838                    return logicList.get(r).getName();
2839                case COMMENT_COLUMN:
2840                    return logicList.get(r).getComment();
2841                default:
2842                    return null;
2843            }
2844        }
2845
2846        @Override
2847        public void setValueAt(Object type, int r, int c) {
2848            var logicList = _groupList.get(_groupRow).getLogicList();
2849            switch (c) {
2850                case LABEL_COLUMN:
2851                    logicList.get(r).setLabel((String) type);
2852                    setDirty(true);
2853                    break;
2854                case OPER_COLUMN:
2855                    var z = (Operator) type;
2856                    if (z != null) {
2857                        if (z.name().startsWith("z")) {
2858                            return;
2859                        }
2860                        if (z.name().equals("x0")) {
2861                            logicList.get(r).setOper(null);
2862                            return;
2863                        }
2864                    }
2865                    logicList.get(r).setOper((Operator) type);
2866                    setDirty(true);
2867                    break;
2868                case NAME_COLUMN:
2869                    logicList.get(r).setName((String) type);
2870                    setDirty(true);
2871                    break;
2872                case COMMENT_COLUMN:
2873                    logicList.get(r).setComment((String) type);
2874                    setDirty(true);
2875                    break;
2876                default:
2877                    break;
2878            }
2879        }
2880
2881        @Override
2882        public boolean isCellEditable(int r, int c) {
2883            return true;
2884        }
2885
2886        public int getPreferredWidth(int col) {
2887            switch (col) {
2888                case LABEL_COLUMN:
2889                    return new JTextField(6).getPreferredSize().width;
2890                case OPER_COLUMN:
2891                    return new JTextField(20).getPreferredSize().width;
2892                case NAME_COLUMN:
2893                case COMMENT_COLUMN:
2894                    return new JTextField(40).getPreferredSize().width;
2895                default:
2896                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2897                    return new JTextField(8).getPreferredSize().width;
2898            }
2899        }
2900    }
2901
2902    /**
2903     * TableModel for Input table entries.
2904     */
2905    class InputModel extends AbstractTableModel {
2906
2907        InputModel() {
2908        }
2909
2910        public static final int INPUT_COLUMN = 0;
2911        public static final int NAME_COLUMN = 1;
2912        public static final int TRUE_COLUMN = 2;
2913        public static final int FALSE_COLUMN = 3;
2914
2915        @Override
2916        public int getRowCount() {
2917            return _inputList.size();
2918        }
2919
2920        @Override
2921        public int getColumnCount() {
2922            return 4;
2923        }
2924
2925        @Override
2926        public Class<?> getColumnClass(int c) {
2927            return String.class;
2928        }
2929
2930        @Override
2931        public String getColumnName(int col) {
2932            switch (col) {
2933                case INPUT_COLUMN:
2934                    return Bundle.getMessage("ColumnInput");  // NOI18N
2935                case NAME_COLUMN:
2936                    return Bundle.getMessage("ColumnName");  // NOI18N
2937                case TRUE_COLUMN:
2938                    return Bundle.getMessage("ColumnTrue");  // NOI18N
2939                case FALSE_COLUMN:
2940                    return Bundle.getMessage("ColumnFalse");  // NOI18N
2941                default:
2942                    return "unknown";  // NOI18N
2943            }
2944        }
2945
2946        @Override
2947        public Object getValueAt(int r, int c) {
2948            switch (c) {
2949                case INPUT_COLUMN:
2950                    int grp = r / 8;
2951                    int rem = r % 8;
2952                    return "I" + grp + "." + rem;
2953                case NAME_COLUMN:
2954                    return _inputList.get(r).getName();
2955                case TRUE_COLUMN:
2956                    var trueID = new EventID(_inputList.get(r).getEventTrue());
2957                    return _nameStore.getEventName(trueID);
2958                case FALSE_COLUMN:
2959                    var falseID = new EventID(_inputList.get(r).getEventFalse());
2960                    return _nameStore.getEventName(falseID);
2961                default:
2962                    return null;
2963            }
2964        }
2965
2966        @Override
2967        public void setValueAt(Object type, int r, int c) {
2968            switch (c) {
2969                case NAME_COLUMN:
2970                    _inputList.get(r).setName((String) type);
2971                    setDirty(true);
2972                    break;
2973                case TRUE_COLUMN:
2974                    var trueEvent = getTableInputEventID((String) type);
2975                    if (trueEvent != null) {
2976                        _inputList.get(r).setEventTrue(trueEvent);
2977                        setDirty(true);
2978                    }
2979                    break;
2980                case FALSE_COLUMN:
2981                    var falseEvent = getTableInputEventID((String) type);
2982                    if (falseEvent != null) {
2983                        _inputList.get(r).setEventFalse(falseEvent);
2984                        setDirty(true);
2985                    }
2986                    break;
2987                default:
2988                    break;
2989            }
2990        }
2991
2992        @Override
2993        public boolean isCellEditable(int r, int c) {
2994            return ((c == NAME_COLUMN) || (c == TRUE_COLUMN) || (c == FALSE_COLUMN));
2995        }
2996
2997        public int getPreferredWidth(int col) {
2998            switch (col) {
2999                case INPUT_COLUMN:
3000                    return new JTextField(6).getPreferredSize().width;
3001                case NAME_COLUMN:
3002                    return new JTextField(50).getPreferredSize().width;
3003                case TRUE_COLUMN:
3004                case FALSE_COLUMN:
3005                    return new JTextField(20).getPreferredSize().width;
3006                default:
3007                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
3008                    return new JTextField(8).getPreferredSize().width;
3009            }
3010        }
3011    }
3012
3013    /**
3014     * TableModel for Output table entries.
3015     */
3016    class OutputModel extends AbstractTableModel {
3017        OutputModel() {
3018        }
3019
3020        public static final int OUTPUT_COLUMN = 0;
3021        public static final int NAME_COLUMN = 1;
3022        public static final int TRUE_COLUMN = 2;
3023        public static final int FALSE_COLUMN = 3;
3024
3025        @Override
3026        public int getRowCount() {
3027            return _outputList.size();
3028        }
3029
3030        @Override
3031        public int getColumnCount() {
3032            return 4;
3033        }
3034
3035        @Override
3036        public Class<?> getColumnClass(int c) {
3037            return String.class;
3038        }
3039
3040        @Override
3041        public String getColumnName(int col) {
3042            switch (col) {
3043                case OUTPUT_COLUMN:
3044                    return Bundle.getMessage("ColumnOutput");  // NOI18N
3045                case NAME_COLUMN:
3046                    return Bundle.getMessage("ColumnName");  // NOI18N
3047                case TRUE_COLUMN:
3048                    return Bundle.getMessage("ColumnTrue");  // NOI18N
3049                case FALSE_COLUMN:
3050                    return Bundle.getMessage("ColumnFalse");  // NOI18N
3051                default:
3052                    return "unknown";  // NOI18N
3053            }
3054        }
3055
3056        @Override
3057        public Object getValueAt(int r, int c) {
3058            switch (c) {
3059                case OUTPUT_COLUMN:
3060                    int grp = r / 8;
3061                    int rem = r % 8;
3062                    return "Q" + grp + "." + rem;
3063                case NAME_COLUMN:
3064                    return _outputList.get(r).getName();
3065                case TRUE_COLUMN:
3066                    var trueID = new EventID(_outputList.get(r).getEventTrue());
3067                    return _nameStore.getEventName(trueID);
3068                case FALSE_COLUMN:
3069                    var falseID = new EventID(_outputList.get(r).getEventFalse());
3070                    return _nameStore.getEventName(falseID);
3071                default:
3072                    return null;
3073            }
3074        }
3075
3076        @Override
3077        public void setValueAt(Object type, int r, int c) {
3078            switch (c) {
3079                case NAME_COLUMN:
3080                    _outputList.get(r).setName((String) type);
3081                    setDirty(true);
3082                    break;
3083                case TRUE_COLUMN:
3084                    var trueEvent = getTableInputEventID((String) type);
3085                    if (trueEvent != null) {
3086                        _outputList.get(r).setEventTrue(trueEvent);
3087                        setDirty(true);
3088                    }
3089                    break;
3090                case FALSE_COLUMN:
3091                    var falseEvent = getTableInputEventID((String) type);
3092                    if (falseEvent != null) {
3093                        _outputList.get(r).setEventFalse(falseEvent);
3094                        setDirty(true);
3095                    }
3096                    break;
3097                default:
3098                    break;
3099            }
3100        }
3101
3102        @Override
3103        public boolean isCellEditable(int r, int c) {
3104            return ((c == NAME_COLUMN) || (c == TRUE_COLUMN) || (c == FALSE_COLUMN));
3105        }
3106
3107        public int getPreferredWidth(int col) {
3108            switch (col) {
3109                case OUTPUT_COLUMN:
3110                    return new JTextField(6).getPreferredSize().width;
3111                case NAME_COLUMN:
3112                    return new JTextField(50).getPreferredSize().width;
3113                case TRUE_COLUMN:
3114                case FALSE_COLUMN:
3115                    return new JTextField(20).getPreferredSize().width;
3116                default:
3117                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
3118                    return new JTextField(8).getPreferredSize().width;
3119            }
3120        }
3121    }
3122
3123    /**
3124     * TableModel for circuit receiver table entries.
3125     */
3126    class ReceiverModel extends AbstractTableModel {
3127
3128        ReceiverModel() {
3129        }
3130
3131        public static final int CIRCUIT_COLUMN = 0;
3132        public static final int NAME_COLUMN = 1;
3133        public static final int EVENTID_COLUMN = 2;
3134
3135        @Override
3136        public int getRowCount() {
3137            return _receiverList.size();
3138        }
3139
3140        @Override
3141        public int getColumnCount() {
3142            return 3;
3143        }
3144
3145        @Override
3146        public Class<?> getColumnClass(int c) {
3147            return String.class;
3148        }
3149
3150        @Override
3151        public String getColumnName(int col) {
3152            switch (col) {
3153                case CIRCUIT_COLUMN:
3154                    return Bundle.getMessage("ColumnCircuit");  // NOI18N
3155                case NAME_COLUMN:
3156                    return Bundle.getMessage("ColumnName");  // NOI18N
3157                case EVENTID_COLUMN:
3158                    return Bundle.getMessage("ColumnEventID");  // NOI18N
3159                default:
3160                    return "unknown";  // NOI18N
3161            }
3162        }
3163
3164        @Override
3165        public Object getValueAt(int r, int c) {
3166            switch (c) {
3167                case CIRCUIT_COLUMN:
3168                    return "Y" + r;
3169                case NAME_COLUMN:
3170                    return _receiverList.get(r).getName();
3171                case EVENTID_COLUMN:
3172                    var eventID = new EventID(_receiverList.get(r).getEventId());
3173                    return _nameStore.getEventName(eventID);
3174                default:
3175                    return null;
3176            }
3177        }
3178
3179        @Override
3180        public void setValueAt(Object type, int r, int c) {
3181            switch (c) {
3182                case NAME_COLUMN:
3183                    _receiverList.get(r).setName((String) type);
3184                    setDirty(true);
3185                    break;
3186                case EVENTID_COLUMN:
3187                    var event = getTableInputEventID((String) type);
3188                    if (event != null) {
3189                        _receiverList.get(r).setEventId(event);
3190                        setDirty(true);
3191                    }
3192                    break;
3193                default:
3194                    break;
3195            }
3196        }
3197
3198        @Override
3199        public boolean isCellEditable(int r, int c) {
3200            return ((c == NAME_COLUMN) || (c == EVENTID_COLUMN));
3201        }
3202
3203        public int getPreferredWidth(int col) {
3204            switch (col) {
3205                case CIRCUIT_COLUMN:
3206                    return new JTextField(6).getPreferredSize().width;
3207                case NAME_COLUMN:
3208                    return new JTextField(50).getPreferredSize().width;
3209                case EVENTID_COLUMN:
3210                    return new JTextField(20).getPreferredSize().width;
3211                default:
3212                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
3213                    return new JTextField(8).getPreferredSize().width;
3214            }
3215        }
3216    }
3217
3218    /**
3219     * TableModel for circuit transmitter table entries.
3220     */
3221    class TransmitterModel extends AbstractTableModel {
3222
3223        TransmitterModel() {
3224        }
3225
3226        public static final int CIRCUIT_COLUMN = 0;
3227        public static final int NAME_COLUMN = 1;
3228        public static final int EVENTID_COLUMN = 2;
3229
3230        @Override
3231        public int getRowCount() {
3232            return _transmitterList.size();
3233        }
3234
3235        @Override
3236        public int getColumnCount() {
3237            return 3;
3238        }
3239
3240        @Override
3241        public Class<?> getColumnClass(int c) {
3242            return String.class;
3243        }
3244
3245        @Override
3246        public String getColumnName(int col) {
3247            switch (col) {
3248                case CIRCUIT_COLUMN:
3249                    return Bundle.getMessage("ColumnCircuit");  // NOI18N
3250                case NAME_COLUMN:
3251                    return Bundle.getMessage("ColumnName");  // NOI18N
3252                case EVENTID_COLUMN:
3253                    return Bundle.getMessage("ColumnEventID");  // NOI18N
3254                default:
3255                    return "unknown";  // NOI18N
3256            }
3257        }
3258
3259        @Override
3260        public Object getValueAt(int r, int c) {
3261            switch (c) {
3262                case CIRCUIT_COLUMN:
3263                    return "Z" + r;
3264                case NAME_COLUMN:
3265                    return _transmitterList.get(r).getName();
3266                case EVENTID_COLUMN:
3267                    var eventID = new EventID(_transmitterList.get(r).getEventId());
3268                    return _nameStore.getEventName(eventID);
3269                default:
3270                    return null;
3271            }
3272        }
3273
3274        @Override
3275        public void setValueAt(Object type, int r, int c) {
3276            switch (c) {
3277                case NAME_COLUMN:
3278                    _transmitterList.get(r).setName((String) type);
3279                    setDirty(true);
3280                    break;
3281                case EVENTID_COLUMN:
3282                    var event = getTableInputEventID((String) type);
3283                    if (event != null) {
3284                        _transmitterList.get(r).setEventId(event);
3285                        setDirty(true);
3286                    }
3287                    break;
3288                default:
3289                    break;
3290            }
3291        }
3292
3293        @Override
3294        public boolean isCellEditable(int r, int c) {
3295            return ((c == NAME_COLUMN) || (c == EVENTID_COLUMN));
3296        }
3297
3298        public int getPreferredWidth(int col) {
3299            switch (col) {
3300                case CIRCUIT_COLUMN:
3301                    return new JTextField(6).getPreferredSize().width;
3302                case NAME_COLUMN:
3303                    return new JTextField(50).getPreferredSize().width;
3304                case EVENTID_COLUMN:
3305                    return new JTextField(20).getPreferredSize().width;
3306                default:
3307                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
3308                    return new JTextField(8).getPreferredSize().width;
3309            }
3310        }
3311    }
3312
3313    // --------------  Operator Enum ---------
3314
3315    public enum Operator {
3316        x0(Bundle.getMessage("Separator0")),
3317        z1(Bundle.getMessage("Separator1")),
3318        A(Bundle.getMessage("OperatorA")),
3319        AN(Bundle.getMessage("OperatorAN")),
3320        O(Bundle.getMessage("OperatorO")),
3321        ON(Bundle.getMessage("OperatorON")),
3322        X(Bundle.getMessage("OperatorX")),
3323        XN(Bundle.getMessage("OperatorXN")),
3324
3325        z2(Bundle.getMessage("Separator2")),    // The STL parens are represented by lower case p
3326        Ap(Bundle.getMessage("OperatorAp")),
3327        ANp(Bundle.getMessage("OperatorANp")),
3328        Op(Bundle.getMessage("OperatorOp")),
3329        ONp(Bundle.getMessage("OperatorONp")),
3330        Xp(Bundle.getMessage("OperatorXp")),
3331        XNp(Bundle.getMessage("OperatorXNp")),
3332        Cp(Bundle.getMessage("OperatorCp")),    // Close paren
3333
3334        z3(Bundle.getMessage("Separator3")),
3335        EQ(Bundle.getMessage("OperatorEQ")),    // = operator
3336        R(Bundle.getMessage("OperatorR")),
3337        S(Bundle.getMessage("OperatorS")),
3338
3339        z4(Bundle.getMessage("Separator4")),
3340        NOT(Bundle.getMessage("OperatorNOT")),
3341        SET(Bundle.getMessage("OperatorSET")),
3342        CLR(Bundle.getMessage("OperatorCLR")),
3343        SAVE(Bundle.getMessage("OperatorSAVE")),
3344
3345        z5(Bundle.getMessage("Separator5")),
3346        JU(Bundle.getMessage("OperatorJU")),
3347        JC(Bundle.getMessage("OperatorJC")),
3348        JCN(Bundle.getMessage("OperatorJCN")),
3349        JCB(Bundle.getMessage("OperatorJCB")),
3350        JNB(Bundle.getMessage("OperatorJNB")),
3351        JBI(Bundle.getMessage("OperatorJBI")),
3352        JNBI(Bundle.getMessage("OperatorJNBI")),
3353
3354        z6(Bundle.getMessage("Separator6")),
3355        FN(Bundle.getMessage("OperatorFN")),
3356        FP(Bundle.getMessage("OperatorFP")),
3357
3358        z7(Bundle.getMessage("Separator7")),
3359        L(Bundle.getMessage("OperatorL")),
3360        FR(Bundle.getMessage("OperatorFR")),
3361        SP(Bundle.getMessage("OperatorSP")),
3362        SE(Bundle.getMessage("OperatorSE")),
3363        SD(Bundle.getMessage("OperatorSD")),
3364        SS(Bundle.getMessage("OperatorSS")),
3365        SF(Bundle.getMessage("OperatorSF"));
3366
3367        private final String _text;
3368
3369        private Operator(String text) {
3370            this._text = text;
3371        }
3372
3373        @Override
3374        public String toString() {
3375            return _text;
3376        }
3377
3378    }
3379
3380    // --------------  Token Class ---------
3381
3382    static class Token {
3383        String _type = "";
3384        String _name = "";
3385        int _offsetStart = 0;
3386        int _offsetEnd = 0;
3387
3388        Token(String type, String name, int offsetStart, int offsetEnd) {
3389            _type = type;
3390            _name = name;
3391            _offsetStart = offsetStart;
3392            _offsetEnd = offsetEnd;
3393        }
3394
3395        public String getType() {
3396            return _type;
3397        }
3398
3399        public String getName() {
3400            return _name;
3401        }
3402
3403        public int getStart() {
3404            return _offsetStart;
3405        }
3406
3407        public int getEnd() {
3408            return _offsetEnd;
3409        }
3410
3411        @Override
3412        public String toString() {
3413            return String.format("Type: %s, Name: %s, Start: %d, End: %d",
3414                    _type, _name, _offsetStart, _offsetEnd);
3415        }
3416    }
3417
3418    // --------------  misc items ---------
3419    @Override
3420    public java.util.List<JMenu> getMenus() {
3421        // create a file menu
3422        var retval = new ArrayList<JMenu>();
3423        var fileMenu = new JMenu(Bundle.getMessage("MenuFile"));
3424
3425        _refreshItem = new JMenuItem(Bundle.getMessage("MenuRefresh"));
3426        _storeItem = new JMenuItem(Bundle.getMessage("MenuStore"));
3427        _importItem = new JMenuItem(Bundle.getMessage("MenuImport"));
3428        _exportItem = new JMenuItem(Bundle.getMessage("MenuExport"));
3429        _loadItem = new JMenuItem(Bundle.getMessage("MenuLoad"));
3430
3431        _refreshItem.addActionListener(this::pushedRefreshButton);
3432        _storeItem.addActionListener(this::pushedStoreButton);
3433        _importItem.addActionListener(this::pushedImportButton);
3434        _exportItem.addActionListener(this::pushedExportButton);
3435        _loadItem.addActionListener(this::loadBackupData);
3436
3437        fileMenu.add(_refreshItem);
3438        fileMenu.add(_storeItem);
3439        fileMenu.addSeparator();
3440        fileMenu.add(_importItem);
3441        fileMenu.add(_exportItem);
3442        fileMenu.addSeparator();
3443        fileMenu.add(_loadItem);
3444
3445        _refreshItem.setEnabled(false);
3446        _storeItem.setEnabled(false);
3447        _exportItem.setEnabled(false);
3448
3449        var viewMenu = new JMenu(Bundle.getMessage("MenuView"));
3450
3451        // Create a radio button menu group
3452        ButtonGroup viewButtonGroup = new ButtonGroup();
3453
3454        _viewSingle.setActionCommand("SINGLE");
3455        _viewSingle.addItemListener(this::setViewMode);
3456        viewMenu.add(_viewSingle);
3457        viewButtonGroup.add(_viewSingle);
3458
3459        _viewSplit.setActionCommand("SPLIT");
3460        _viewSplit.addItemListener(this::setViewMode);
3461        viewMenu.add(_viewSplit);
3462        viewButtonGroup.add(_viewSplit);
3463
3464        // Select the current view
3465        if (_splitView) {
3466            _viewSplit.setSelected(true);
3467        } else {
3468            _viewSingle.setSelected(true);
3469        }
3470
3471        viewMenu.addSeparator();
3472
3473        _viewPreview.addItemListener(this::setPreview);
3474        viewMenu.add(_viewPreview);
3475
3476        // Set the current preview menu item state
3477        if (_stlPreview) {
3478            _viewPreview.setSelected(true);
3479        } else {
3480            _viewPreview.setSelected(false);
3481        }
3482
3483        viewMenu.addSeparator();
3484
3485        // Create a radio button menu group
3486        ButtonGroup viewStoreGroup = new ButtonGroup();
3487
3488        _viewReadable.setActionCommand("LINE");
3489        _viewReadable.addItemListener(this::setViewStoreMode);
3490        viewMenu.add(_viewReadable);
3491        viewStoreGroup.add(_viewReadable);
3492
3493        _viewCompact.setActionCommand("CLNE");
3494        _viewCompact.addItemListener(this::setViewStoreMode);
3495        viewMenu.add(_viewCompact);
3496        viewStoreGroup.add(_viewCompact);
3497
3498        _viewCompressed.setActionCommand("COMP");
3499        _viewCompressed.addItemListener(this::setViewStoreMode);
3500        viewMenu.add(_viewCompressed);
3501        viewStoreGroup.add(_viewCompressed);
3502
3503        // Select the current store mode
3504        switch (_storeMode) {
3505            case "LINE":
3506                _viewReadable.setSelected(true);
3507                break;
3508            case "CLNE":
3509                _viewCompact.setSelected(true);
3510                break;
3511            case "COMP":
3512                _viewCompressed.setSelected(true);
3513                break;
3514            default:
3515                log.error("Invalid store mode: {}", _storeMode);
3516        }
3517
3518        retval.add(fileMenu);
3519        retval.add(viewMenu);
3520
3521        return retval;
3522    }
3523
3524    private void setViewMode(ItemEvent e) {
3525        if (e.getStateChange() == ItemEvent.SELECTED) {
3526            var button = (JRadioButtonMenuItem) e.getItem();
3527            var cmd = button.getActionCommand();
3528            _splitView = "SPLIT".equals(cmd);
3529            _pm.setProperty(this.getClass().getName(), "ViewMode", cmd);
3530            if (_splitView) {
3531                splitTabs();
3532            } else if (_detailTabs.getTabCount() == 1) {
3533                mergeTabs();
3534            }
3535        }
3536    }
3537
3538    private void splitTabs() {
3539        if (_detailTabs.getTabCount() == 5) {
3540            _detailTabs.remove(4);
3541            _detailTabs.remove(3);
3542            _detailTabs.remove(2);
3543            _detailTabs.remove(1);
3544        }
3545
3546        if (_tableTabs == null) {
3547            _tableTabs = new JTabbedPane();
3548        }
3549
3550        _tableTabs.add(Bundle.getMessage("ButtonI"), _inputPanel);  // NOI18N
3551        _tableTabs.add(Bundle.getMessage("ButtonQ"), _outputPanel);  // NOI18N
3552        _tableTabs.add(Bundle.getMessage("ButtonY"), _receiverPanel);  // NOI18N
3553        _tableTabs.add(Bundle.getMessage("ButtonZ"), _transmitterPanel);  // NOI18N
3554
3555        _tableTabs.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
3556
3557        var tablePanel = new JPanel();
3558        tablePanel.setLayout(new BorderLayout());
3559        tablePanel.add(_tableTabs, BorderLayout.CENTER);
3560
3561        if (_tableFrame == null) {
3562            _tableFrame = new JmriJFrame(Bundle.getMessage("TitleTables"));
3563            _tableFrame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
3564        }
3565        _tableFrame.add(tablePanel);
3566        _tableFrame.pack();
3567        _tableFrame.setVisible(true);
3568    }
3569
3570    private void mergeTabs() {
3571        if (_tableTabs != null) {
3572            _tableTabs.removeAll();
3573        }
3574
3575        _detailTabs.add(Bundle.getMessage("ButtonI"), _inputPanel);  // NOI18N
3576        _detailTabs.add(Bundle.getMessage("ButtonQ"), _outputPanel);  // NOI18N
3577        _detailTabs.add(Bundle.getMessage("ButtonY"), _receiverPanel);  // NOI18N
3578        _detailTabs.add(Bundle.getMessage("ButtonZ"), _transmitterPanel);  // NOI18N
3579
3580        if (_tableFrame != null) {
3581            _tableFrame.setVisible(false);
3582        }
3583    }
3584
3585    private void setPreview(ItemEvent e) {
3586        if (e.getStateChange() == ItemEvent.SELECTED) {
3587            _stlPreview = true;
3588
3589            _stlTextArea = new JTextArea();
3590            _stlTextArea.setEditable(false);
3591            _stlTextArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
3592            _stlTextArea.setMargin(new Insets(5,10,0,0));
3593
3594            var previewPanel = new JPanel();
3595            previewPanel.setLayout(new BorderLayout());
3596            previewPanel.add(_stlTextArea, BorderLayout.CENTER);
3597
3598            if (_previewFrame == null) {
3599                _previewFrame = new JmriJFrame(Bundle.getMessage("TitlePreview"));
3600                _previewFrame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
3601            }
3602            _previewFrame.add(previewPanel);
3603            _previewFrame.pack();
3604            _previewFrame.setVisible(true);
3605        } else {
3606            _stlPreview = false;
3607
3608            if (_previewFrame != null) {
3609                _previewFrame.setVisible(false);
3610            }
3611        }
3612        _pm.setSimplePreferenceState(_previewModeCheck, _stlPreview);
3613    }
3614
3615    private void setViewStoreMode(ItemEvent e) {
3616        if (e.getStateChange() == ItemEvent.SELECTED) {
3617            var button = (JRadioButtonMenuItem) e.getItem();
3618            var cmd = button.getActionCommand();
3619            _storeMode = cmd;
3620            _pm.setProperty(this.getClass().getName(), "StoreMode", cmd);
3621        }
3622    }
3623
3624    @Override
3625    public void dispose() {
3626        if (_tableFrame != null) {
3627            _tableFrame.dispose();
3628        }
3629        if (_previewFrame != null) {
3630            _previewFrame.dispose();
3631        }
3632        super.dispose();
3633    }
3634
3635    @Override
3636    public String getHelpTarget() {
3637        return "package.jmri.jmrix.openlcb.swing.stleditor.StlEditorPane";
3638    }
3639
3640    @Override
3641    public String getTitle() {
3642        if (_canMemo != null) {
3643            return (_canMemo.getUserName() + " STL Editor");
3644        }
3645        return Bundle.getMessage("TitleSTLEditor");
3646    }
3647
3648    /**
3649     * Nested class to create one of these using old-style defaults
3650     */
3651    public static class Default extends jmri.jmrix.can.swing.CanNamedPaneAction {
3652
3653        public Default() {
3654            super("STL Editor",
3655                    new jmri.util.swing.sdi.JmriJFrameInterface(),
3656                    StlEditorPane.class.getName(),
3657                    jmri.InstanceManager.getNullableDefault(jmri.jmrix.can.CanSystemConnectionMemo.class));
3658        }
3659
3660        public Default(String name, jmri.util.swing.WindowInterface iface) {
3661            super(name,
3662                    iface,
3663                    StlEditorPane.class.getName(),
3664                    jmri.InstanceManager.getNullableDefault(jmri.jmrix.can.CanSystemConnectionMemo.class));
3665        }
3666
3667        public Default(String name, Icon icon, jmri.util.swing.WindowInterface iface) {
3668            super(name,
3669                    icon, iface,
3670                    StlEditorPane.class.getName(),
3671                    jmri.InstanceManager.getNullableDefault(jmri.jmrix.can.CanSystemConnectionMemo.class));
3672        }
3673    }
3674
3675    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(StlEditorPane.class);
3676}