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