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