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