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