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