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