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