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