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