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