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