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