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