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                csvFile.printRecord("", logic.getLabel(), operName, logic.getName(), logic.getComment());
2211            }
2212        }
2213
2214        // Flush the write buffer and close the file
2215        csvFile.flush();
2216        csvFile.close();
2217    }
2218
2219    private void exportInputs() throws IOException {
2220        var fileWriter = new FileWriter(_csvDirectoryPath + "inputs.csv");
2221        var bufferedWriter = new BufferedWriter(fileWriter);
2222        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
2223
2224        csvFile.printRecord(Bundle.getMessage("ColumnInput"), Bundle.getMessage("ColumnName"),
2225                 Bundle.getMessage("ColumnTrue"), Bundle.getMessage("ColumnFalse"));
2226
2227        for (int i = 0; i < 16; i++) {
2228            for (int j = 0; j < 8; j++) {
2229                var variable = "I" + i + "." + j;
2230                var row = _inputList.get((i * 8) + j);
2231                csvFile.printRecord(variable, row.getName(), row.getEventTrue(), row.getEventFalse());
2232            }
2233        }
2234
2235        // Flush the write buffer and close the file
2236        csvFile.flush();
2237        csvFile.close();
2238    }
2239
2240    private void exportOutputs() throws IOException {
2241        var fileWriter = new FileWriter(_csvDirectoryPath + "outputs.csv");
2242        var bufferedWriter = new BufferedWriter(fileWriter);
2243        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
2244
2245        csvFile.printRecord(Bundle.getMessage("ColumnOutput"), Bundle.getMessage("ColumnName"),
2246                 Bundle.getMessage("ColumnTrue"), Bundle.getMessage("ColumnFalse"));
2247
2248        for (int i = 0; i < 16; i++) {
2249            for (int j = 0; j < 8; j++) {
2250                var variable = "Q" + i + "." + j;
2251                var row = _outputList.get((i * 8) + j);
2252                csvFile.printRecord(variable, row.getName(), row.getEventTrue(), row.getEventFalse());
2253            }
2254        }
2255
2256        // Flush the write buffer and close the file
2257        csvFile.flush();
2258        csvFile.close();
2259    }
2260
2261    private void exportReceivers() throws IOException {
2262        var fileWriter = new FileWriter(_csvDirectoryPath + "receivers.csv");
2263        var bufferedWriter = new BufferedWriter(fileWriter);
2264        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
2265
2266        csvFile.printRecord(Bundle.getMessage("ColumnCircuit"), Bundle.getMessage("ColumnName"),
2267                 Bundle.getMessage("ColumnEventID"));
2268
2269        for (int i = 0; i < 16; i++) {
2270            var variable = "Y" + i;
2271            var row = _receiverList.get(i);
2272            csvFile.printRecord(variable, row.getName(), row.getEventId());
2273        }
2274
2275        // Flush the write buffer and close the file
2276        csvFile.flush();
2277        csvFile.close();
2278    }
2279
2280    private void exportTransmitters() throws IOException {
2281        var fileWriter = new FileWriter(_csvDirectoryPath + "transmitters.csv");
2282        var bufferedWriter = new BufferedWriter(fileWriter);
2283        var csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
2284
2285        csvFile.printRecord(Bundle.getMessage("ColumnCircuit"), Bundle.getMessage("ColumnName"),
2286                 Bundle.getMessage("ColumnEventID"));
2287
2288        for (int i = 0; i < 16; i++) {
2289            var variable = "Z" + i;
2290            var row = _transmitterList.get(i);
2291            csvFile.printRecord(variable, row.getName(), row.getEventId());
2292        }
2293
2294        // Flush the write buffer and close the file
2295        csvFile.flush();
2296        csvFile.close();
2297    }
2298
2299    /**
2300     * Select the directory that will be used for the CSV file set.
2301     * @param isOpen - True for CSV Import and false for CSV Export.
2302     * @return true if a directory was selected.
2303     */
2304    private boolean setCsvDirectoryPath(boolean isOpen) {
2305        var directoryChooser = new JmriJFileChooser(FileUtil.getUserFilesPath());
2306        directoryChooser.setApproveButtonText(Bundle.getMessage("SelectCsvButton"));
2307        directoryChooser.setDialogTitle(Bundle.getMessage("SelectCsvTitle"));
2308        directoryChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
2309
2310        int response = 0;
2311        if (isOpen) {
2312            response = directoryChooser.showOpenDialog(this);
2313        } else {
2314            response = directoryChooser.showSaveDialog(this);
2315        }
2316        if (response != JFileChooser.APPROVE_OPTION) {
2317            return false;
2318        }
2319        _csvDirectoryPath = directoryChooser.getSelectedFile().getAbsolutePath() + FileUtil.SEPARATOR;
2320
2321        return true;
2322    }
2323
2324    // --------------  Data Utilities ---------
2325
2326    private void setDirty(boolean dirty) {
2327        _dirty = dirty;
2328    }
2329
2330    private boolean isDirty() {
2331        return _dirty;
2332    }
2333
2334    private boolean replaceData() {
2335        if (isDirty()) {
2336            int response = JmriJOptionPane.showConfirmDialog(this,
2337                    Bundle.getMessage("MessageRevert"),
2338                    Bundle.getMessage("TitleRevert"),
2339                    JmriJOptionPane.YES_NO_OPTION);
2340            if (response != JmriJOptionPane.YES_OPTION) {
2341                return false;
2342            }
2343        }
2344        return true;
2345    }
2346
2347    private void warningDialog(String title, String message) {
2348        JmriJOptionPane.showMessageDialog(this,
2349            message,
2350            title,
2351            JmriJOptionPane.WARNING_MESSAGE);
2352    }
2353
2354    // --------------  Data validation ---------
2355
2356    static boolean isLabelValid(String label) {
2357        if (label.isEmpty()) {
2358            return true;
2359        }
2360
2361        var match = PARSE_LABEL.matcher(label);
2362        if (match.find()) {
2363            return true;
2364        }
2365
2366        JmriJOptionPane.showMessageDialog(null,
2367                Bundle.getMessage("MessageLabel", label),
2368                Bundle.getMessage("TitleLabel"),
2369                JmriJOptionPane.ERROR_MESSAGE);
2370        return false;
2371    }
2372
2373    static boolean isEventValid(String event) {
2374        var valid = true;
2375
2376        if (event.isEmpty()) {
2377            return valid;
2378        }
2379
2380        var hexPairs = event.split("\\.");
2381        if (hexPairs.length != 8) {
2382            valid = false;
2383        } else {
2384            for (int i = 0; i < 8; i++) {
2385                var match = PARSE_HEXPAIR.matcher(hexPairs[i]);
2386                if (!match.find()) {
2387                    valid = false;
2388                    break;
2389                }
2390            }
2391        }
2392
2393        return valid;
2394    }
2395
2396    // --------------  table lists ---------
2397
2398    /**
2399     * The Group row contains the name and the raw data for one of the 16 groups.
2400     * It also contains the decoded logic for the group in the logic list.
2401     */
2402    static class GroupRow {
2403        String _name;
2404        String _multiLine = "";
2405        List<LogicRow> _logicList = new ArrayList<>();
2406
2407
2408        GroupRow(String name) {
2409            _name = name;
2410        }
2411
2412        String getName() {
2413            return _name;
2414        }
2415
2416        void setName(String newName) {
2417            _name = newName;
2418        }
2419
2420        List<LogicRow> getLogicList() {
2421            return _logicList;
2422        }
2423
2424        void setLogicList(List<LogicRow> logicList) {
2425            _logicList.clear();
2426            _logicList.addAll(logicList);
2427        }
2428
2429        void clearLogicList() {
2430            _logicList.clear();
2431        }
2432
2433        String getMultiLine() {
2434            return _multiLine;
2435        }
2436
2437        void setMultiLine(String newMultiLine) {
2438            _multiLine = newMultiLine.strip();
2439        }
2440
2441        String getSize() {
2442            int size = (_multiLine.length() * 100) / 255;
2443            return String.valueOf(size) + "%";
2444        }
2445    }
2446
2447    /**
2448     * The definition of a logic row
2449     */
2450    static class LogicRow {
2451        String _label;
2452        Operator _oper;
2453        String _name;
2454        String _comment;
2455
2456        LogicRow(String label, Operator oper, String name, String comment) {
2457            _label = label;
2458            _oper = oper;
2459            _name = name;
2460            _comment = comment;
2461        }
2462
2463        String getLabel() {
2464            return _label;
2465        }
2466
2467        void setLabel(String newLabel) {
2468            var label = newLabel.trim();
2469            if (isLabelValid(label)) {
2470                _label = label;
2471            }
2472        }
2473
2474        Operator getOper() {
2475            return _oper;
2476        }
2477
2478        String getOperName() {
2479            if (_oper == null) {
2480                return "";
2481            }
2482
2483            String operName = _oper.name();
2484
2485            // Fix special enums
2486            if (operName.equals("Cp")) {
2487                operName = ")";
2488            } else if (operName.equals("EQ")) {
2489                operName = "=";
2490            } else if (operName.contains("p")) {
2491                operName = operName.replace("p", "(");
2492            }
2493
2494            return operName;
2495        }
2496
2497        void setOper(Operator newOper) {
2498            _oper = newOper;
2499        }
2500
2501        String getName() {
2502            return _name;
2503        }
2504
2505        void setName(String newName) {
2506            _name = newName.trim();
2507        }
2508
2509        String getComment() {
2510            return _comment;
2511        }
2512
2513        void setComment(String newComment) {
2514            _comment = newComment;
2515        }
2516    }
2517
2518    /**
2519     * The name and assigned true and false events for an Input.
2520     */
2521    static class InputRow {
2522        String _name;
2523        String _eventTrue;
2524        String _eventFalse;
2525
2526        InputRow(String name, String eventTrue, String eventFalse) {
2527            _name = name;
2528            _eventTrue = eventTrue;
2529            _eventFalse = eventFalse;
2530        }
2531
2532        String getName() {
2533            return _name;
2534        }
2535
2536        void setName(String newName) {
2537            _name = newName.trim();
2538        }
2539
2540        String getEventTrue() {
2541            if (_eventTrue.length() == 0) return "00.00.00.00.00.00.00.00";
2542            return _eventTrue;
2543        }
2544
2545        void setEventTrue(String newEventTrue) {
2546            _eventTrue = newEventTrue.trim();
2547        }
2548
2549        String getEventFalse() {
2550            if (_eventFalse.length() == 0) return "00.00.00.00.00.00.00.00";
2551            return _eventFalse;
2552        }
2553
2554        void setEventFalse(String newEventFalse) {
2555            _eventFalse = newEventFalse.trim();
2556        }
2557    }
2558
2559    /**
2560     * The name and assigned true and false events for an Output.
2561     */
2562    static class OutputRow {
2563        String _name;
2564        String _eventTrue;
2565        String _eventFalse;
2566
2567        OutputRow(String name, String eventTrue, String eventFalse) {
2568            _name = name;
2569            _eventTrue = eventTrue;
2570            _eventFalse = eventFalse;
2571        }
2572
2573        String getName() {
2574            return _name;
2575        }
2576
2577        void setName(String newName) {
2578            _name = newName.trim();
2579        }
2580
2581        String getEventTrue() {
2582            if (_eventTrue.length() == 0) return "00.00.00.00.00.00.00.00";
2583            return _eventTrue;
2584        }
2585
2586        void setEventTrue(String newEventTrue) {
2587            _eventTrue = newEventTrue.trim();
2588        }
2589
2590        String getEventFalse() {
2591            if (_eventFalse.length() == 0) return "00.00.00.00.00.00.00.00";
2592            return _eventFalse;
2593        }
2594
2595        void setEventFalse(String newEventFalse) {
2596            _eventFalse = newEventFalse.trim();
2597        }
2598    }
2599
2600    /**
2601     * The name and assigned event id for a circuit receiver.
2602     */
2603    static class ReceiverRow {
2604        String _name;
2605        String _eventid;
2606
2607        ReceiverRow(String name, String eventid) {
2608            _name = name;
2609            _eventid = eventid;
2610        }
2611
2612        String getName() {
2613            return _name;
2614        }
2615
2616        void setName(String newName) {
2617            _name = newName.trim();
2618        }
2619
2620        String getEventId() {
2621            if (_eventid.length() == 0) return "00.00.00.00.00.00.00.00";
2622            return _eventid;
2623        }
2624
2625        void setEventId(String newEventid) {
2626            _eventid = newEventid.trim();
2627        }
2628    }
2629
2630    /**
2631     * The name and assigned event id for a circuit transmitter.
2632     */
2633    static class TransmitterRow {
2634        String _name;
2635        String _eventid;
2636
2637        TransmitterRow(String name, String eventid) {
2638            _name = name;
2639            _eventid = eventid;
2640        }
2641
2642        String getName() {
2643            return _name;
2644        }
2645
2646        void setName(String newName) {
2647            _name = newName.trim();
2648        }
2649
2650        String getEventId() {
2651            if (_eventid.length() == 0) return "00.00.00.00.00.00.00.00";
2652            return _eventid;
2653        }
2654
2655        void setEventId(String newEventid) {
2656            _eventid = newEventid.trim();
2657        }
2658    }
2659
2660    // --------------  table models ---------
2661
2662    /**
2663     * The table input can be either a valid dotted-hex string or an "event name". If the input is
2664     * an event name, the name has to be converted to a dotted-hex string.  Creating a new event
2665     * name is not supported.
2666     * @param event The dotted-hex or event name string.
2667     * @return the dotted-hex string or null if the event name is not in the name store.
2668     */
2669    private String getTableInputEventID(String event) {
2670        if (isEventValid(event)) {
2671            return event;
2672        }
2673
2674        try {
2675            EventID eventID = _nameStore.getEventID(event);
2676            return eventID.toShortString();
2677        }
2678        catch (NumberFormatException num) {
2679            log.error("STL Editor getTableInputEventID event failed for event name {} (NumberFormatException)", event);
2680        } catch (IllegalArgumentException arg) {
2681            log.error("STL Editor getTableInputEventID event failed for event name {} (IllegalArgumentException)", event);
2682        }
2683
2684        JmriJOptionPane.showMessageDialog(null,
2685                Bundle.getMessage("MessageEventTable", event),
2686                Bundle.getMessage("TitleEventTable"),
2687                JmriJOptionPane.ERROR_MESSAGE);
2688
2689        return null;
2690
2691    }
2692
2693    /**
2694     * TableModel for Group table entries.
2695     */
2696    class GroupModel extends AbstractTableModel {
2697
2698        GroupModel() {
2699        }
2700
2701        public static final int ROW_COLUMN = 0;
2702        public static final int NAME_COLUMN = 1;
2703
2704        @Override
2705        public int getRowCount() {
2706            return _groupList.size();
2707        }
2708
2709        @Override
2710        public int getColumnCount() {
2711            return 2;
2712        }
2713
2714        @Override
2715        public Class<?> getColumnClass(int c) {
2716            return String.class;
2717        }
2718
2719        @Override
2720        public String getColumnName(int col) {
2721            switch (col) {
2722                case ROW_COLUMN:
2723                    return "";
2724                case NAME_COLUMN:
2725                    return Bundle.getMessage("ColumnName");
2726                default:
2727                    return "unknown";  // NOI18N
2728            }
2729        }
2730
2731        @Override
2732        public Object getValueAt(int r, int c) {
2733            switch (c) {
2734                case ROW_COLUMN:
2735                    return r + 1;
2736                case NAME_COLUMN:
2737                    return _groupList.get(r).getName();
2738                default:
2739                    return null;
2740            }
2741        }
2742
2743        @Override
2744        public void setValueAt(Object type, int r, int c) {
2745            switch (c) {
2746                case NAME_COLUMN:
2747                    _groupList.get(r).setName((String) type);
2748                    setDirty(true);
2749                    break;
2750                default:
2751                    break;
2752            }
2753        }
2754
2755        @Override
2756        public boolean isCellEditable(int r, int c) {
2757            return (c == NAME_COLUMN);
2758        }
2759
2760        public int getPreferredWidth(int col) {
2761            switch (col) {
2762                case ROW_COLUMN:
2763                    return new JTextField(4).getPreferredSize().width;
2764                case NAME_COLUMN:
2765                    return new JTextField(20).getPreferredSize().width;
2766                default:
2767                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2768                    return new JTextField(8).getPreferredSize().width;
2769            }
2770        }
2771    }
2772
2773    /**
2774     * TableModel for STL table entries.
2775     */
2776    class LogicModel extends AbstractTableModel {
2777
2778        LogicModel() {
2779        }
2780
2781        public static final int LABEL_COLUMN = 0;
2782        public static final int OPER_COLUMN = 1;
2783        public static final int NAME_COLUMN = 2;
2784        public static final int COMMENT_COLUMN = 3;
2785
2786        @Override
2787        public int getRowCount() {
2788            var logicList = _groupList.get(_groupRow).getLogicList();
2789            return logicList.size();
2790        }
2791
2792        @Override
2793        public int getColumnCount() {
2794            return 4;
2795        }
2796
2797        @Override
2798        public Class<?> getColumnClass(int c) {
2799            if (c == OPER_COLUMN) return JComboBox.class;
2800            return String.class;
2801        }
2802
2803        @Override
2804        public String getColumnName(int col) {
2805            switch (col) {
2806                case LABEL_COLUMN:
2807                    return Bundle.getMessage("ColumnLabel");  // NOI18N
2808                case OPER_COLUMN:
2809                    return Bundle.getMessage("ColumnOper");  // NOI18N
2810                case NAME_COLUMN:
2811                    return Bundle.getMessage("ColumnName");  // NOI18N
2812                case COMMENT_COLUMN:
2813                    return Bundle.getMessage("ColumnComment");  // NOI18N
2814                default:
2815                    return "unknown";  // NOI18N
2816            }
2817        }
2818
2819        @Override
2820        public Object getValueAt(int r, int c) {
2821            var logicList = _groupList.get(_groupRow).getLogicList();
2822            switch (c) {
2823                case LABEL_COLUMN:
2824                    return logicList.get(r).getLabel();
2825                case OPER_COLUMN:
2826                    return logicList.get(r).getOper();
2827                case NAME_COLUMN:
2828                    return logicList.get(r).getName();
2829                case COMMENT_COLUMN:
2830                    return logicList.get(r).getComment();
2831                default:
2832                    return null;
2833            }
2834        }
2835
2836        @Override
2837        public void setValueAt(Object type, int r, int c) {
2838            var logicList = _groupList.get(_groupRow).getLogicList();
2839            switch (c) {
2840                case LABEL_COLUMN:
2841                    logicList.get(r).setLabel((String) type);
2842                    setDirty(true);
2843                    break;
2844                case OPER_COLUMN:
2845                    var z = (Operator) type;
2846                    if (z != null) {
2847                        if (z.name().startsWith("z")) {
2848                            return;
2849                        }
2850                        if (z.name().equals("x0")) {
2851                            logicList.get(r).setOper(null);
2852                            return;
2853                        }
2854                    }
2855                    logicList.get(r).setOper((Operator) type);
2856                    setDirty(true);
2857                    break;
2858                case NAME_COLUMN:
2859                    logicList.get(r).setName((String) type);
2860                    setDirty(true);
2861                    break;
2862                case COMMENT_COLUMN:
2863                    logicList.get(r).setComment((String) type);
2864                    setDirty(true);
2865                    break;
2866                default:
2867                    break;
2868            }
2869        }
2870
2871        @Override
2872        public boolean isCellEditable(int r, int c) {
2873            return true;
2874        }
2875
2876        public int getPreferredWidth(int col) {
2877            switch (col) {
2878                case LABEL_COLUMN:
2879                    return new JTextField(6).getPreferredSize().width;
2880                case OPER_COLUMN:
2881                    return new JTextField(20).getPreferredSize().width;
2882                case NAME_COLUMN:
2883                case COMMENT_COLUMN:
2884                    return new JTextField(40).getPreferredSize().width;
2885                default:
2886                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2887                    return new JTextField(8).getPreferredSize().width;
2888            }
2889        }
2890    }
2891
2892    /**
2893     * TableModel for Input table entries.
2894     */
2895    class InputModel extends AbstractTableModel {
2896
2897        InputModel() {
2898        }
2899
2900        public static final int INPUT_COLUMN = 0;
2901        public static final int NAME_COLUMN = 1;
2902        public static final int TRUE_COLUMN = 2;
2903        public static final int FALSE_COLUMN = 3;
2904
2905        @Override
2906        public int getRowCount() {
2907            return _inputList.size();
2908        }
2909
2910        @Override
2911        public int getColumnCount() {
2912            return 4;
2913        }
2914
2915        @Override
2916        public Class<?> getColumnClass(int c) {
2917            return String.class;
2918        }
2919
2920        @Override
2921        public String getColumnName(int col) {
2922            switch (col) {
2923                case INPUT_COLUMN:
2924                    return Bundle.getMessage("ColumnInput");  // NOI18N
2925                case NAME_COLUMN:
2926                    return Bundle.getMessage("ColumnName");  // NOI18N
2927                case TRUE_COLUMN:
2928                    return Bundle.getMessage("ColumnTrue");  // NOI18N
2929                case FALSE_COLUMN:
2930                    return Bundle.getMessage("ColumnFalse");  // NOI18N
2931                default:
2932                    return "unknown";  // NOI18N
2933            }
2934        }
2935
2936        @Override
2937        public Object getValueAt(int r, int c) {
2938            switch (c) {
2939                case INPUT_COLUMN:
2940                    int grp = r / 8;
2941                    int rem = r % 8;
2942                    return "I" + grp + "." + rem;
2943                case NAME_COLUMN:
2944                    return _inputList.get(r).getName();
2945                case TRUE_COLUMN:
2946                    var trueID = new EventID(_inputList.get(r).getEventTrue());
2947                    return _nameStore.getEventName(trueID);
2948                case FALSE_COLUMN:
2949                    var falseID = new EventID(_inputList.get(r).getEventFalse());
2950                    return _nameStore.getEventName(falseID);
2951                default:
2952                    return null;
2953            }
2954        }
2955
2956        @Override
2957        public void setValueAt(Object type, int r, int c) {
2958            switch (c) {
2959                case NAME_COLUMN:
2960                    _inputList.get(r).setName((String) type);
2961                    setDirty(true);
2962                    break;
2963                case TRUE_COLUMN:
2964                    var trueEvent = getTableInputEventID((String) type);
2965                    if (trueEvent != null) {
2966                        _inputList.get(r).setEventTrue(trueEvent);
2967                        setDirty(true);
2968                    }
2969                    break;
2970                case FALSE_COLUMN:
2971                    var falseEvent = getTableInputEventID((String) type);
2972                    if (falseEvent != null) {
2973                        _inputList.get(r).setEventFalse(falseEvent);
2974                        setDirty(true);
2975                    }
2976                    break;
2977                default:
2978                    break;
2979            }
2980        }
2981
2982        @Override
2983        public boolean isCellEditable(int r, int c) {
2984            return ((c == NAME_COLUMN) || (c == TRUE_COLUMN) || (c == FALSE_COLUMN));
2985        }
2986
2987        public int getPreferredWidth(int col) {
2988            switch (col) {
2989                case INPUT_COLUMN:
2990                    return new JTextField(6).getPreferredSize().width;
2991                case NAME_COLUMN:
2992                    return new JTextField(50).getPreferredSize().width;
2993                case TRUE_COLUMN:
2994                case FALSE_COLUMN:
2995                    return new JTextField(20).getPreferredSize().width;
2996                default:
2997                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
2998                    return new JTextField(8).getPreferredSize().width;
2999            }
3000        }
3001    }
3002
3003    /**
3004     * TableModel for Output table entries.
3005     */
3006    class OutputModel extends AbstractTableModel {
3007        OutputModel() {
3008        }
3009
3010        public static final int OUTPUT_COLUMN = 0;
3011        public static final int NAME_COLUMN = 1;
3012        public static final int TRUE_COLUMN = 2;
3013        public static final int FALSE_COLUMN = 3;
3014
3015        @Override
3016        public int getRowCount() {
3017            return _outputList.size();
3018        }
3019
3020        @Override
3021        public int getColumnCount() {
3022            return 4;
3023        }
3024
3025        @Override
3026        public Class<?> getColumnClass(int c) {
3027            return String.class;
3028        }
3029
3030        @Override
3031        public String getColumnName(int col) {
3032            switch (col) {
3033                case OUTPUT_COLUMN:
3034                    return Bundle.getMessage("ColumnOutput");  // NOI18N
3035                case NAME_COLUMN:
3036                    return Bundle.getMessage("ColumnName");  // NOI18N
3037                case TRUE_COLUMN:
3038                    return Bundle.getMessage("ColumnTrue");  // NOI18N
3039                case FALSE_COLUMN:
3040                    return Bundle.getMessage("ColumnFalse");  // NOI18N
3041                default:
3042                    return "unknown";  // NOI18N
3043            }
3044        }
3045
3046        @Override
3047        public Object getValueAt(int r, int c) {
3048            switch (c) {
3049                case OUTPUT_COLUMN:
3050                    int grp = r / 8;
3051                    int rem = r % 8;
3052                    return "Q" + grp + "." + rem;
3053                case NAME_COLUMN:
3054                    return _outputList.get(r).getName();
3055                case TRUE_COLUMN:
3056                    var trueID = new EventID(_outputList.get(r).getEventTrue());
3057                    return _nameStore.getEventName(trueID);
3058                case FALSE_COLUMN:
3059                    var falseID = new EventID(_outputList.get(r).getEventFalse());
3060                    return _nameStore.getEventName(falseID);
3061                default:
3062                    return null;
3063            }
3064        }
3065
3066        @Override
3067        public void setValueAt(Object type, int r, int c) {
3068            switch (c) {
3069                case NAME_COLUMN:
3070                    _outputList.get(r).setName((String) type);
3071                    setDirty(true);
3072                    break;
3073                case TRUE_COLUMN:
3074                    var trueEvent = getTableInputEventID((String) type);
3075                    if (trueEvent != null) {
3076                        _outputList.get(r).setEventTrue(trueEvent);
3077                        setDirty(true);
3078                    }
3079                    break;
3080                case FALSE_COLUMN:
3081                    var falseEvent = getTableInputEventID((String) type);
3082                    if (falseEvent != null) {
3083                        _outputList.get(r).setEventFalse(falseEvent);
3084                        setDirty(true);
3085                    }
3086                    break;
3087                default:
3088                    break;
3089            }
3090        }
3091
3092        @Override
3093        public boolean isCellEditable(int r, int c) {
3094            return ((c == NAME_COLUMN) || (c == TRUE_COLUMN) || (c == FALSE_COLUMN));
3095        }
3096
3097        public int getPreferredWidth(int col) {
3098            switch (col) {
3099                case OUTPUT_COLUMN:
3100                    return new JTextField(6).getPreferredSize().width;
3101                case NAME_COLUMN:
3102                    return new JTextField(50).getPreferredSize().width;
3103                case TRUE_COLUMN:
3104                case FALSE_COLUMN:
3105                    return new JTextField(20).getPreferredSize().width;
3106                default:
3107                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
3108                    return new JTextField(8).getPreferredSize().width;
3109            }
3110        }
3111    }
3112
3113    /**
3114     * TableModel for circuit receiver table entries.
3115     */
3116    class ReceiverModel extends AbstractTableModel {
3117
3118        ReceiverModel() {
3119        }
3120
3121        public static final int CIRCUIT_COLUMN = 0;
3122        public static final int NAME_COLUMN = 1;
3123        public static final int EVENTID_COLUMN = 2;
3124
3125        @Override
3126        public int getRowCount() {
3127            return _receiverList.size();
3128        }
3129
3130        @Override
3131        public int getColumnCount() {
3132            return 3;
3133        }
3134
3135        @Override
3136        public Class<?> getColumnClass(int c) {
3137            return String.class;
3138        }
3139
3140        @Override
3141        public String getColumnName(int col) {
3142            switch (col) {
3143                case CIRCUIT_COLUMN:
3144                    return Bundle.getMessage("ColumnCircuit");  // NOI18N
3145                case NAME_COLUMN:
3146                    return Bundle.getMessage("ColumnName");  // NOI18N
3147                case EVENTID_COLUMN:
3148                    return Bundle.getMessage("ColumnEventID");  // NOI18N
3149                default:
3150                    return "unknown";  // NOI18N
3151            }
3152        }
3153
3154        @Override
3155        public Object getValueAt(int r, int c) {
3156            switch (c) {
3157                case CIRCUIT_COLUMN:
3158                    return "Y" + r;
3159                case NAME_COLUMN:
3160                    return _receiverList.get(r).getName();
3161                case EVENTID_COLUMN:
3162                    var eventID = new EventID(_receiverList.get(r).getEventId());
3163                    return _nameStore.getEventName(eventID);
3164                default:
3165                    return null;
3166            }
3167        }
3168
3169        @Override
3170        public void setValueAt(Object type, int r, int c) {
3171            switch (c) {
3172                case NAME_COLUMN:
3173                    _receiverList.get(r).setName((String) type);
3174                    setDirty(true);
3175                    break;
3176                case EVENTID_COLUMN:
3177                    var event = getTableInputEventID((String) type);
3178                    if (event != null) {
3179                        _receiverList.get(r).setEventId(event);
3180                        setDirty(true);
3181                    }
3182                    break;
3183                default:
3184                    break;
3185            }
3186        }
3187
3188        @Override
3189        public boolean isCellEditable(int r, int c) {
3190            return ((c == NAME_COLUMN) || (c == EVENTID_COLUMN));
3191        }
3192
3193        public int getPreferredWidth(int col) {
3194            switch (col) {
3195                case CIRCUIT_COLUMN:
3196                    return new JTextField(6).getPreferredSize().width;
3197                case NAME_COLUMN:
3198                    return new JTextField(50).getPreferredSize().width;
3199                case EVENTID_COLUMN:
3200                    return new JTextField(20).getPreferredSize().width;
3201                default:
3202                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
3203                    return new JTextField(8).getPreferredSize().width;
3204            }
3205        }
3206    }
3207
3208    /**
3209     * TableModel for circuit transmitter table entries.
3210     */
3211    class TransmitterModel extends AbstractTableModel {
3212
3213        TransmitterModel() {
3214        }
3215
3216        public static final int CIRCUIT_COLUMN = 0;
3217        public static final int NAME_COLUMN = 1;
3218        public static final int EVENTID_COLUMN = 2;
3219
3220        @Override
3221        public int getRowCount() {
3222            return _transmitterList.size();
3223        }
3224
3225        @Override
3226        public int getColumnCount() {
3227            return 3;
3228        }
3229
3230        @Override
3231        public Class<?> getColumnClass(int c) {
3232            return String.class;
3233        }
3234
3235        @Override
3236        public String getColumnName(int col) {
3237            switch (col) {
3238                case CIRCUIT_COLUMN:
3239                    return Bundle.getMessage("ColumnCircuit");  // NOI18N
3240                case NAME_COLUMN:
3241                    return Bundle.getMessage("ColumnName");  // NOI18N
3242                case EVENTID_COLUMN:
3243                    return Bundle.getMessage("ColumnEventID");  // NOI18N
3244                default:
3245                    return "unknown";  // NOI18N
3246            }
3247        }
3248
3249        @Override
3250        public Object getValueAt(int r, int c) {
3251            switch (c) {
3252                case CIRCUIT_COLUMN:
3253                    return "Z" + r;
3254                case NAME_COLUMN:
3255                    return _transmitterList.get(r).getName();
3256                case EVENTID_COLUMN:
3257                    var eventID = new EventID(_transmitterList.get(r).getEventId());
3258                    return _nameStore.getEventName(eventID);
3259                default:
3260                    return null;
3261            }
3262        }
3263
3264        @Override
3265        public void setValueAt(Object type, int r, int c) {
3266            switch (c) {
3267                case NAME_COLUMN:
3268                    _transmitterList.get(r).setName((String) type);
3269                    setDirty(true);
3270                    break;
3271                case EVENTID_COLUMN:
3272                    var event = getTableInputEventID((String) type);
3273                    if (event != null) {
3274                        _transmitterList.get(r).setEventId(event);
3275                        setDirty(true);
3276                    }
3277                    break;
3278                default:
3279                    break;
3280            }
3281        }
3282
3283        @Override
3284        public boolean isCellEditable(int r, int c) {
3285            return ((c == NAME_COLUMN) || (c == EVENTID_COLUMN));
3286        }
3287
3288        public int getPreferredWidth(int col) {
3289            switch (col) {
3290                case CIRCUIT_COLUMN:
3291                    return new JTextField(6).getPreferredSize().width;
3292                case NAME_COLUMN:
3293                    return new JTextField(50).getPreferredSize().width;
3294                case EVENTID_COLUMN:
3295                    return new JTextField(20).getPreferredSize().width;
3296                default:
3297                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
3298                    return new JTextField(8).getPreferredSize().width;
3299            }
3300        }
3301    }
3302
3303    // --------------  Operator Enum ---------
3304
3305    public enum Operator {
3306        x0(Bundle.getMessage("Separator0")),
3307        z1(Bundle.getMessage("Separator1")),
3308        A(Bundle.getMessage("OperatorA")),
3309        AN(Bundle.getMessage("OperatorAN")),
3310        O(Bundle.getMessage("OperatorO")),
3311        ON(Bundle.getMessage("OperatorON")),
3312        X(Bundle.getMessage("OperatorX")),
3313        XN(Bundle.getMessage("OperatorXN")),
3314
3315        z2(Bundle.getMessage("Separator2")),    // The STL parens are represented by lower case p
3316        Ap(Bundle.getMessage("OperatorAp")),
3317        ANp(Bundle.getMessage("OperatorANp")),
3318        Op(Bundle.getMessage("OperatorOp")),
3319        ONp(Bundle.getMessage("OperatorONp")),
3320        Xp(Bundle.getMessage("OperatorXp")),
3321        XNp(Bundle.getMessage("OperatorXNp")),
3322        Cp(Bundle.getMessage("OperatorCp")),    // Close paren
3323
3324        z3(Bundle.getMessage("Separator3")),
3325        EQ(Bundle.getMessage("OperatorEQ")),    // = operator
3326        R(Bundle.getMessage("OperatorR")),
3327        S(Bundle.getMessage("OperatorS")),
3328
3329        z4(Bundle.getMessage("Separator4")),
3330        NOT(Bundle.getMessage("OperatorNOT")),
3331        SET(Bundle.getMessage("OperatorSET")),
3332        CLR(Bundle.getMessage("OperatorCLR")),
3333        SAVE(Bundle.getMessage("OperatorSAVE")),
3334
3335        z5(Bundle.getMessage("Separator5")),
3336        JU(Bundle.getMessage("OperatorJU")),
3337        JC(Bundle.getMessage("OperatorJC")),
3338        JCN(Bundle.getMessage("OperatorJCN")),
3339        JCB(Bundle.getMessage("OperatorJCB")),
3340        JNB(Bundle.getMessage("OperatorJNB")),
3341        JBI(Bundle.getMessage("OperatorJBI")),
3342        JNBI(Bundle.getMessage("OperatorJNBI")),
3343
3344        z6(Bundle.getMessage("Separator6")),
3345        FN(Bundle.getMessage("OperatorFN")),
3346        FP(Bundle.getMessage("OperatorFP")),
3347
3348        z7(Bundle.getMessage("Separator7")),
3349        L(Bundle.getMessage("OperatorL")),
3350        FR(Bundle.getMessage("OperatorFR")),
3351        SP(Bundle.getMessage("OperatorSP")),
3352        SE(Bundle.getMessage("OperatorSE")),
3353        SD(Bundle.getMessage("OperatorSD")),
3354        SS(Bundle.getMessage("OperatorSS")),
3355        SF(Bundle.getMessage("OperatorSF"));
3356
3357        private final String _text;
3358
3359        private Operator(String text) {
3360            this._text = text;
3361        }
3362
3363        @Override
3364        public String toString() {
3365            return _text;
3366        }
3367
3368    }
3369
3370    // --------------  Token Class ---------
3371
3372    static class Token {
3373        String _type = "";
3374        String _name = "";
3375        int _offsetStart = 0;
3376        int _offsetEnd = 0;
3377
3378        Token(String type, String name, int offsetStart, int offsetEnd) {
3379            _type = type;
3380            _name = name;
3381            _offsetStart = offsetStart;
3382            _offsetEnd = offsetEnd;
3383        }
3384
3385        public String getType() {
3386            return _type;
3387        }
3388
3389        public String getName() {
3390            return _name;
3391        }
3392
3393        public int getStart() {
3394            return _offsetStart;
3395        }
3396
3397        public int getEnd() {
3398            return _offsetEnd;
3399        }
3400
3401        @Override
3402        public String toString() {
3403            return String.format("Type: %s, Name: %s, Start: %d, End: %d",
3404                    _type, _name, _offsetStart, _offsetEnd);
3405        }
3406    }
3407
3408    // --------------  misc items ---------
3409    @Override
3410    public java.util.List<JMenu> getMenus() {
3411        // create a file menu
3412        var retval = new ArrayList<JMenu>();
3413        var fileMenu = new JMenu(Bundle.getMessage("MenuFile"));
3414
3415        _refreshItem = new JMenuItem(Bundle.getMessage("MenuRefresh"));
3416        _storeItem = new JMenuItem(Bundle.getMessage("MenuStore"));
3417        _importItem = new JMenuItem(Bundle.getMessage("MenuImport"));
3418        _exportItem = new JMenuItem(Bundle.getMessage("MenuExport"));
3419        _loadItem = new JMenuItem(Bundle.getMessage("MenuLoad"));
3420
3421        _refreshItem.addActionListener(this::pushedRefreshButton);
3422        _storeItem.addActionListener(this::pushedStoreButton);
3423        _importItem.addActionListener(this::pushedImportButton);
3424        _exportItem.addActionListener(this::pushedExportButton);
3425        _loadItem.addActionListener(this::loadBackupData);
3426
3427        fileMenu.add(_refreshItem);
3428        fileMenu.add(_storeItem);
3429        fileMenu.addSeparator();
3430        fileMenu.add(_importItem);
3431        fileMenu.add(_exportItem);
3432        fileMenu.addSeparator();
3433        fileMenu.add(_loadItem);
3434
3435        _refreshItem.setEnabled(false);
3436        _storeItem.setEnabled(false);
3437        _exportItem.setEnabled(false);
3438
3439        var viewMenu = new JMenu(Bundle.getMessage("MenuView"));
3440
3441        // Create a radio button menu group
3442        ButtonGroup viewButtonGroup = new ButtonGroup();
3443
3444        _viewSingle.setActionCommand("SINGLE");
3445        _viewSingle.addItemListener(this::setViewMode);
3446        viewMenu.add(_viewSingle);
3447        viewButtonGroup.add(_viewSingle);
3448
3449        _viewSplit.setActionCommand("SPLIT");
3450        _viewSplit.addItemListener(this::setViewMode);
3451        viewMenu.add(_viewSplit);
3452        viewButtonGroup.add(_viewSplit);
3453
3454        // Select the current view
3455        if (_splitView) {
3456            _viewSplit.setSelected(true);
3457        } else {
3458            _viewSingle.setSelected(true);
3459        }
3460
3461        viewMenu.addSeparator();
3462
3463        _viewPreview.addItemListener(this::setPreview);
3464        viewMenu.add(_viewPreview);
3465
3466        // Set the current preview menu item state
3467        if (_stlPreview) {
3468            _viewPreview.setSelected(true);
3469        } else {
3470            _viewPreview.setSelected(false);
3471        }
3472
3473        viewMenu.addSeparator();
3474
3475        // Create a radio button menu group
3476        ButtonGroup viewStoreGroup = new ButtonGroup();
3477
3478        _viewReadable.setActionCommand("LINE");
3479        _viewReadable.addItemListener(this::setViewStoreMode);
3480        viewMenu.add(_viewReadable);
3481        viewStoreGroup.add(_viewReadable);
3482
3483        _viewCompact.setActionCommand("CLNE");
3484        _viewCompact.addItemListener(this::setViewStoreMode);
3485        viewMenu.add(_viewCompact);
3486        viewStoreGroup.add(_viewCompact);
3487
3488        _viewCompressed.setActionCommand("COMP");
3489        _viewCompressed.addItemListener(this::setViewStoreMode);
3490        viewMenu.add(_viewCompressed);
3491        viewStoreGroup.add(_viewCompressed);
3492
3493        // Select the current store mode
3494        switch (_storeMode) {
3495            case "LINE":
3496                _viewReadable.setSelected(true);
3497                break;
3498            case "CLNE":
3499                _viewCompact.setSelected(true);
3500                break;
3501            case "COMP":
3502                _viewCompressed.setSelected(true);
3503                break;
3504            default:
3505                log.error("Invalid store mode: {}", _storeMode);
3506        }
3507
3508        retval.add(fileMenu);
3509        retval.add(viewMenu);
3510
3511        return retval;
3512    }
3513
3514    private void setViewMode(ItemEvent e) {
3515        if (e.getStateChange() == ItemEvent.SELECTED) {
3516            var button = (JRadioButtonMenuItem) e.getItem();
3517            var cmd = button.getActionCommand();
3518            _splitView = "SPLIT".equals(cmd);
3519            _pm.setProperty(this.getClass().getName(), "ViewMode", cmd);
3520            if (_splitView) {
3521                splitTabs();
3522            } else if (_detailTabs.getTabCount() == 1) {
3523                mergeTabs();
3524            }
3525        }
3526    }
3527
3528    private void splitTabs() {
3529        if (_detailTabs.getTabCount() == 5) {
3530            _detailTabs.remove(4);
3531            _detailTabs.remove(3);
3532            _detailTabs.remove(2);
3533            _detailTabs.remove(1);
3534        }
3535
3536        if (_tableTabs == null) {
3537            _tableTabs = new JTabbedPane();
3538        }
3539
3540        _tableTabs.add(Bundle.getMessage("ButtonI"), _inputPanel);  // NOI18N
3541        _tableTabs.add(Bundle.getMessage("ButtonQ"), _outputPanel);  // NOI18N
3542        _tableTabs.add(Bundle.getMessage("ButtonY"), _receiverPanel);  // NOI18N
3543        _tableTabs.add(Bundle.getMessage("ButtonZ"), _transmitterPanel);  // NOI18N
3544
3545        _tableTabs.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
3546
3547        var tablePanel = new JPanel();
3548        tablePanel.setLayout(new BorderLayout());
3549        tablePanel.add(_tableTabs, BorderLayout.CENTER);
3550
3551        if (_tableFrame == null) {
3552            _tableFrame = new JmriJFrame(Bundle.getMessage("TitleTables"));
3553            _tableFrame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
3554        }
3555        _tableFrame.add(tablePanel);
3556        _tableFrame.pack();
3557        _tableFrame.setVisible(true);
3558    }
3559
3560    private void mergeTabs() {
3561        if (_tableTabs != null) {
3562            _tableTabs.removeAll();
3563        }
3564
3565        _detailTabs.add(Bundle.getMessage("ButtonI"), _inputPanel);  // NOI18N
3566        _detailTabs.add(Bundle.getMessage("ButtonQ"), _outputPanel);  // NOI18N
3567        _detailTabs.add(Bundle.getMessage("ButtonY"), _receiverPanel);  // NOI18N
3568        _detailTabs.add(Bundle.getMessage("ButtonZ"), _transmitterPanel);  // NOI18N
3569
3570        if (_tableFrame != null) {
3571            _tableFrame.setVisible(false);
3572        }
3573    }
3574
3575    private void setPreview(ItemEvent e) {
3576        if (e.getStateChange() == ItemEvent.SELECTED) {
3577            _stlPreview = true;
3578
3579            _stlTextArea = new JTextArea();
3580            _stlTextArea.setEditable(false);
3581            _stlTextArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
3582            _stlTextArea.setMargin(new Insets(5,10,0,0));
3583
3584            var previewPanel = new JPanel();
3585            previewPanel.setLayout(new BorderLayout());
3586            previewPanel.add(_stlTextArea, BorderLayout.CENTER);
3587
3588            if (_previewFrame == null) {
3589                _previewFrame = new JmriJFrame(Bundle.getMessage("TitlePreview"));
3590                _previewFrame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
3591            }
3592            _previewFrame.add(previewPanel);
3593            _previewFrame.pack();
3594            _previewFrame.setVisible(true);
3595        } else {
3596            _stlPreview = false;
3597
3598            if (_previewFrame != null) {
3599                _previewFrame.setVisible(false);
3600            }
3601        }
3602        _pm.setSimplePreferenceState(_previewModeCheck, _stlPreview);
3603    }
3604
3605    private void setViewStoreMode(ItemEvent e) {
3606        if (e.getStateChange() == ItemEvent.SELECTED) {
3607            var button = (JRadioButtonMenuItem) e.getItem();
3608            var cmd = button.getActionCommand();
3609            _storeMode = cmd;
3610            _pm.setProperty(this.getClass().getName(), "StoreMode", cmd);
3611        }
3612    }
3613
3614    @Override
3615    public void dispose() {
3616        if (_tableFrame != null) {
3617            _tableFrame.dispose();
3618        }
3619        if (_previewFrame != null) {
3620            _previewFrame.dispose();
3621        }
3622        super.dispose();
3623    }
3624
3625    @Override
3626    public String getHelpTarget() {
3627        return "package.jmri.jmrix.openlcb.swing.stleditor.StlEditorPane";
3628    }
3629
3630    @Override
3631    public String getTitle() {
3632        if (_canMemo != null) {
3633            return (_canMemo.getUserName() + " STL Editor");
3634        }
3635        return Bundle.getMessage("TitleSTLEditor");
3636    }
3637
3638    /**
3639     * Nested class to create one of these using old-style defaults
3640     */
3641    public static class Default extends jmri.jmrix.can.swing.CanNamedPaneAction {
3642
3643        public Default() {
3644            super("STL Editor",
3645                    new jmri.util.swing.sdi.JmriJFrameInterface(),
3646                    StlEditorPane.class.getName(),
3647                    jmri.InstanceManager.getNullableDefault(jmri.jmrix.can.CanSystemConnectionMemo.class));
3648        }
3649
3650        public Default(String name, jmri.util.swing.WindowInterface iface) {
3651            super(name,
3652                    iface,
3653                    StlEditorPane.class.getName(),
3654                    jmri.InstanceManager.getNullableDefault(jmri.jmrix.can.CanSystemConnectionMemo.class));
3655        }
3656
3657        public Default(String name, Icon icon, jmri.util.swing.WindowInterface iface) {
3658            super(name,
3659                    icon, iface,
3660                    StlEditorPane.class.getName(),
3661                    jmri.InstanceManager.getNullableDefault(jmri.jmrix.can.CanSystemConnectionMemo.class));
3662        }
3663    }
3664
3665    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(StlEditorPane.class);
3666}