001package jmri.jmrix.openlcb.swing.lccpro; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004 005import java.awt.*; 006import java.awt.event.*; 007import java.awt.datatransfer.Transferable; 008 009import java.beans.PropertyChangeEvent; 010import java.beans.PropertyChangeListener; 011import java.io.*; 012import java.nio.charset.StandardCharsets; 013import java.util.ArrayList; 014 015import javax.swing.*; 016import javax.swing.event.ListSelectionEvent; 017 018import jmri.InstanceManager; 019import jmri.ShutDownManager; 020import jmri.UserPreferencesManager; 021 022import jmri.swing.ConnectionLabel; 023import jmri.swing.JTablePersistenceManager; 024import jmri.swing.RowSorterUtil; 025 026import jmri.jmrix.ActiveSystemsMenu; 027import jmri.jmrix.ConnectionConfig; 028import jmri.jmrix.ConnectionConfigManager; 029import jmri.jmrix.can.CanSystemConnectionMemo; 030import jmri.jmrix.openlcb.OlcbNodeGroupStore; 031import jmri.jmrix.openlcb.swing.TrafficStatusLabel; 032 033import jmri.util.*; 034import jmri.util.datatransfer.RosterEntrySelection; 035import jmri.util.swing.*; 036import jmri.util.swing.multipane.TwoPaneTBWindow; 037 038import org.apache.commons.csv.CSVFormat; 039import org.apache.commons.csv.CSVPrinter; 040 041import org.openlcb.*; 042 043/** 044 * A window for LCC Network management. 045 * 046 * @author Bob Jacobsen Copyright (C) 2024 047 */ 048public class LccProFrame extends TwoPaneTBWindow { 049 050 static final ArrayList<LccProFrame> frameInstances = new ArrayList<>(); 051 protected boolean allowQuit = true; 052 protected JmriAbstractAction newWindowAction; 053 054 CanSystemConnectionMemo memo; 055 MimicNodeStore nodestore; 056 OlcbNodeGroupStore groupStore; 057 058 public LccProFrame(String name) { 059 this(name, 060 jmri.InstanceManager.getNullableDefault(jmri.jmrix.can.CanSystemConnectionMemo.class)); 061 } 062 063 public LccProFrame(String name, CanSystemConnectionMemo memo) { 064 this(name, 065 "xml/config/parts/apps/gui3/lccpro/LccProFrameMenu.xml", 066 "xml/config/parts/apps/gui3/lccpro/LccProFrameToolBar.xml", 067 memo); 068 } 069 070 public LccProFrame(String name, String menubarFile, String toolbarFile) { 071 this(name, menubarFile, toolbarFile, jmri.InstanceManager.getNullableDefault(jmri.jmrix.can.CanSystemConnectionMemo.class)); 072 } 073 074 public LccProFrame(String name, String menubarFile, String toolbarFile, CanSystemConnectionMemo memo) { 075 super(name, menubarFile, toolbarFile); 076 this.memo = memo; 077 if (memo == null) { 078 // a functional LccFrame can't be created without an LCC ConnectionConfig 079 javax.swing.JOptionPane.showMessageDialog(this, "LccPro requires a configured LCC or OpenLCB connection, will quit now", 080 "LccPro", JOptionPane.ERROR_MESSAGE); 081 // and close the program 082 // This is justified because this should never happen in a properly 083 // built application: The existence of an LCC/OpenLCB connection 084 // should have been checked long before reaching this point. 085 InstanceManager.getDefault(jmri.ShutDownManager.class).shutdown(); 086 return; 087 } 088 this.nodestore = memo.get(MimicNodeStore.class); 089 this.groupStore = InstanceManager.getDefault(OlcbNodeGroupStore.class); 090 this.allowInFrameServlet = false; 091 prefsMgr = InstanceManager.getDefault(UserPreferencesManager.class); 092 this.setTitle(name); 093 this.buildWindow(); 094 } 095 096 final NodeInfoPane nodeInfoPane = new NodeInfoPane(); 097 final NodePipPane nodePipPane = new NodePipPane(); 098 JLabel firstHelpLabel; 099 int groupSplitPaneLocation = 0; 100 boolean hideGroups = false; 101 final JTextPane id = new JTextPane(); 102 UserPreferencesManager prefsMgr; 103 final java.util.ResourceBundle rb = java.util.ResourceBundle.getBundle("apps.AppsBundle"); 104 // the three parts of the bottom half 105 final JPanel bottomPanel = new JPanel(); 106 JSplitPane bottomLCPanel; // left and center parts 107 JSplitPane bottomRPanel; // right part 108 // main center window (TODO: rename this; TODO: Does this still need to be split?) 109 JSplitPane rosterGroupSplitPane; 110 111 LccProTable nodetable; // node table in center of screen 112 113 JComboBox<String> matchGroupName; // required group name to display; index <= 0 is all 114 115 final JLabel statusField = new JLabel(); 116 static final Dimension summaryPaneDim = new Dimension(0, 170); 117 118 protected void additionsToToolBar() { 119 getToolBar().add(Box.createHorizontalGlue()); 120 } 121 122 /** 123 * For use when the DP3 window is called from another JMRI instance, set 124 * this to prevent the DP3 from shutting down JMRI when the window is 125 * closed. 126 * 127 * @param quitAllowed true if closing window should quit application; false 128 * otherwise 129 */ 130 protected void allowQuit(boolean quitAllowed) { 131 if (allowQuit != quitAllowed) { 132 newWindowAction = null; 133 allowQuit = quitAllowed; 134 } 135 136 firePropertyChange("quit", "setEnabled", allowQuit); 137 //if we are not allowing quit, ie opened from JMRI classic 138 //then we must at least allow the window to be closed 139 if (!allowQuit) { 140 firePropertyChange("closewindow", "setEnabled", true); 141 } 142 } 143 144 // Create right side of the bottom panel 145 146 JPanel bottomRight() { 147 JPanel panel = new JPanel(); 148 panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); 149 panel.setAlignmentX(SwingConstants.LEFT); 150 151 var searchPanel = new JPanel(); 152 searchPanel.setLayout(new WrapLayout()); 153 searchPanel.add(new JLabel(Bundle.getMessage("FrameSearchNodeNames"))); 154 var searchField = new JTextField(12) { 155 @Override 156 public Dimension getMaximumSize() { 157 Dimension size = super.getMaximumSize(); 158 size.height = getPreferredSize().height; 159 return size; 160 } 161 }; 162 searchField.getDocument().putProperty("filterNewlines", Boolean.TRUE); 163 searchField.addKeyListener(new KeyListener() { 164 @Override 165 public void keyTyped(KeyEvent keyEvent) { 166 } 167 168 @Override 169 public void keyReleased(KeyEvent keyEvent) { 170 // on release so the searchField has been updated 171 log.debug("keyTyped {} content {}", keyEvent.getKeyCode(), searchField.getText()); 172 String search = searchField.getText().toLowerCase(); 173 // start search process 174 int count = nodetable.getModel().getRowCount(); 175 for (int row = 0; row < count; row++) { 176 String value = ((String)nodetable.getTable().getValueAt(row, LccProTableModel.NAMECOL)).toLowerCase(); 177 if (value.startsWith(search)) { 178 log.trace(" Hit value {} on {}", value, row); 179 nodetable.getTable().setRowSelectionInterval(row, row); 180 nodetable.getTable().scrollRectToVisible(nodetable.getTable().getCellRect(row,LccProTableModel.NAMECOL, true)); 181 return; 182 } 183 } 184 // here we didn't find anything 185 nodetable.getTable().clearSelection(); 186 } 187 188 @Override 189 public void keyPressed(KeyEvent keyEvent) { 190 } 191 }); 192 searchPanel.add(searchField); 193 panel.add(searchPanel); 194 195 196 var groupPanel = new JPanel(); 197 groupPanel.setLayout(new WrapLayout()); 198 JLabel display = new JLabel(Bundle.getMessage("FrameDisplayNodeGroups")); 199 display.setToolTipText(Bundle.getMessage("FrameDisplayNodeGroupsTt")); 200 groupPanel.add(display); 201 202 matchGroupName = new JComboBox<>(); 203 updateMatchGroupName(); // before adding listener 204 matchGroupName.addActionListener((ActionEvent e) -> { 205 filter(); 206 }); 207 groupStore.addPropertyChangeListener((PropertyChangeEvent evt) -> { 208 updateMatchGroupName(); 209 }); 210 groupPanel.add(matchGroupName); 211 panel.add(groupPanel); 212 213 panel.add(Box.createVerticalGlue()); 214 215 return panel; 216 } 217 218 // load updateMatchGroup combobox with current contents 219 protected void updateMatchGroupName() { 220 matchGroupName.removeAllItems(); 221 matchGroupName.addItem(Bundle.getMessage("FrameAllGroups")); 222 223 var list = groupStore.getGroupNames(); 224 for (String group : list) { 225 matchGroupName.addItem(group); 226 } 227 } 228 229 protected final void buildWindow() { 230 //Additions to the toolbar need to be added first otherwise when trying to hide bits up during the initialisation they remain on screen 231 additionsToToolBar(); 232 frameInstances.add(this); 233 getTop().add(createTop()); 234 getBottom().setMinimumSize(summaryPaneDim); 235 getBottom().add(createBottom()); 236 statusBar(); 237 systemsMenu(); 238 helpMenu(getMenu(), this); 239 240 if (prefsMgr.getSimplePreferenceState(this.getClass().getName() + ".hideSummary")) { 241 //We have to set it to display first, then we can hide it. 242 hideBottomPane(false); 243 hideBottomPane(true); 244 } 245 PropertyChangeListener propertyChangeListener = (PropertyChangeEvent changeEvent) -> { 246 JSplitPane sourceSplitPane = (JSplitPane) changeEvent.getSource(); 247 String propertyName = changeEvent.getPropertyName(); 248 if (propertyName.equals(JSplitPane.LAST_DIVIDER_LOCATION_PROPERTY)) { 249 int current = sourceSplitPane.getDividerLocation() + sourceSplitPane.getDividerSize(); 250 int panesize = (int) (sourceSplitPane.getSize().getHeight()); 251 hideBottomPane = panesize - current <= 1; 252 //p.setSimplePreferenceState(DecoderPro3Window.class.getName()+".hideSummary",hideSummary); 253 } 254 }; 255 256 getSplitPane().addPropertyChangeListener(propertyChangeListener); 257 if (frameInstances.size() > 1) { 258 firePropertyChange("closewindow", "setEnabled", true); 259 allowQuit(frameInstances.get(0).isAllowQuit()); 260 } else { 261 firePropertyChange("closewindow", "setEnabled", false); 262 } 263 } 264 265 //@TODO The disabling of the closeWindow menu item doesn't quite work as this in only invoked on the closing window, and not the one that is left 266 void closeWindow(WindowEvent e) { 267 saveWindowDetails(); 268 if (allowQuit && frameInstances.size() == 1 && !InstanceManager.getDefault(ShutDownManager.class).isShuttingDown()) { 269 handleQuit(e); 270 } else { 271 //As we are not the last window open or we are not allowed to quit the application then we will just close the current window 272 frameInstances.remove(this); 273 super.windowClosing(e); 274 if ((frameInstances.size() == 1) && (allowQuit)) { 275 frameInstances.get(0).firePropertyChange("closewindow", "setEnabled", false); 276 } 277 dispose(); 278 } 279 } 280 281 JComponent createBottom() { 282 JPanel leftPanel = nodeInfoPane; 283 JPanel centerPanel = nodePipPane; 284 JPanel rightPanel = bottomRight(); 285 286 bottomLCPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, centerPanel); 287 bottomRPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, bottomLCPanel, rightPanel); 288 289 leftPanel.setBorder(BorderFactory.createLineBorder(Color.black)); 290 centerPanel.setBorder(BorderFactory.createLineBorder(Color.black)); 291 bottomLCPanel.setBorder(null); 292 293 bottomLCPanel.setResizeWeight(0.67); // determined empirically 294 bottomRPanel.setResizeWeight(0.75); 295 296 bottomLCPanel.setOneTouchExpandable(true); 297 bottomRPanel.setOneTouchExpandable(true); 298 299 // load split locations from preferences 300 Object w = prefsMgr.getProperty(getWindowFrameRef(), "bottomLCPanelDividerLocation"); 301 if (w != null) { 302 var splitPaneLocation = (Integer) w; 303 bottomLCPanel.setDividerLocation(splitPaneLocation); 304 } 305 w = prefsMgr.getProperty(getWindowFrameRef(), "bottomRPanelDividerLocation"); 306 if (w != null) { 307 var splitPaneLocation = (Integer) w; 308 bottomRPanel.setDividerLocation(splitPaneLocation); 309 } 310 311 // add listeners that will store location preferences 312 bottomLCPanel.addPropertyChangeListener((PropertyChangeEvent changeEvent) -> { 313 String propertyName = changeEvent.getPropertyName(); 314 if (propertyName.equals("dividerLocation")) { 315 prefsMgr.setProperty(getWindowFrameRef(), "bottomLCPanelDividerLocation", bottomLCPanel.getDividerLocation()); 316 } 317 }); 318 bottomRPanel.addPropertyChangeListener((PropertyChangeEvent changeEvent) -> { 319 String propertyName = changeEvent.getPropertyName(); 320 if (propertyName.equals("dividerLocation")) { 321 prefsMgr.setProperty(getWindowFrameRef(), "bottomRPanelDividerLocation", bottomRPanel.getDividerLocation()); 322 } 323 }); 324 return bottomRPanel; 325 } 326 327 JComponent createTop() { 328 final JPanel rosters = new JPanel(); 329 rosters.setLayout(new BorderLayout()); 330 // set up node table 331 nodetable = new LccProTable(memo); 332 rosters.add(nodetable, BorderLayout.CENTER); 333 // add selection listener to display selected row 334 nodetable.getTable().getSelectionModel().addListSelectionListener((ListSelectionEvent e) -> { 335 JTable table = nodetable.getTable(); 336 if (!e.getValueIsAdjusting()) { 337 if (table.getSelectedRow() >= 0) { 338 int row = table.convertRowIndexToModel(table.getSelectedRow()); 339 log.debug("Selected: {}", row); 340 MimicNodeStore.NodeMemo nodememo = nodestore.getNodeMemos().toArray(new MimicNodeStore.NodeMemo[0])[row]; 341 log.trace(" node: {}", nodememo.getNodeID().toString()); 342 nodeInfoPane.update(nodememo); 343 nodePipPane.update(nodememo); 344 } 345 } 346 }); 347 348 // Set all the sort and width details of the table first. 349 String nodetableref = getWindowFrameRef() + ":nodes"; 350 nodetable.getTable().setName(nodetableref); 351 352 // Allow only one column to be sorted at a time - 353 // Java allows multiple column sorting, but to effectively persist that, we 354 // need to be intelligent about which columns can be meaningfully sorted 355 // with other columns; this bypasses the problem by only allowing the 356 // last column sorted to affect sorting 357 RowSorterUtil.addSingleSortableColumnListener(nodetable.getTable().getRowSorter()); 358 359 // Reset and then persist the table's ui state 360 JTablePersistenceManager tpm = InstanceManager.getNullableDefault(JTablePersistenceManager.class); 361 if (tpm != null) { 362 tpm.resetState(nodetable.getTable()); 363 tpm.persist(nodetable.getTable()); 364 } 365 nodetable.getTable().setDragEnabled(true); 366 nodetable.getTable().setTransferHandler(new TransferHandler() { 367 368 @Override 369 public int getSourceActions(JComponent c) { 370 return TransferHandler.COPY; 371 } 372 373 @Override 374 public Transferable createTransferable(JComponent c) { 375 JTable table = nodetable.getTable(); 376 ArrayList<String> Ids = new ArrayList<>(table.getSelectedRowCount()); 377 for (int i = 0; i < table.getSelectedRowCount(); i++) { 378 // TODO replace this with something about the nodes to be dragged and dropped 379 // Ids.add(nodetable.getModel().getValueAt(table.getRowSorter().convertRowIndexToModel(table.getSelectedRows()[i]), RostenodetableModel.IDCOL).toString()); 380 } 381 return new RosterEntrySelection(Ids); 382 } 383 384 @Override 385 public void exportDone(JComponent c, Transferable t, int action) { 386 // nothing to do 387 } 388 }); 389 nodetable.getTable().addMouseListener(JmriMouseListener.adapt(new NodePopupListener())); 390 391 // assemble roster/groups splitpane 392 // TODO - figure out what to do with the left side of this and expand the following 393 JPanel leftSide = new JPanel(); 394 leftSide.setEnabled(false); 395 leftSide.setVisible(false); 396 397 rosterGroupSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftSide, rosters); 398 rosterGroupSplitPane.setOneTouchExpandable(false); // TODO set this true once the leftSide is in use 399 rosterGroupSplitPane.setResizeWeight(0); // emphasize right side (nodes) 400 401 Object w = prefsMgr.getProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation"); 402 if (w != null) { 403 groupSplitPaneLocation = (Integer) w; 404 rosterGroupSplitPane.setDividerLocation(groupSplitPaneLocation); 405 } 406 407 log.trace("createTop returns {}", rosterGroupSplitPane); 408 return rosterGroupSplitPane; 409 } 410 411 /** 412 * Set up filtering of displayed rows by group level 413 */ 414 private void filter() { 415 RowFilter<LccProTableModel, Integer> rf = new RowFilter<LccProTableModel, Integer>() { 416 /** 417 * @return true if row is to be displayed 418 */ 419 @Override 420 public boolean include(RowFilter.Entry<? extends LccProTableModel, ? extends Integer> entry) { 421 422 // check for group match 423 if ( matchGroupName.getSelectedIndex() > 0) { // -1 is empty combobox 424 String group = matchGroupName.getSelectedItem().toString(); 425 NodeID node = new NodeID((String)entry.getValue(LccProTableModel.IDCOL)); 426 if ( ! groupStore.isNodeInGroup(node, group)) { 427 return false; 428 } 429 } 430 431 // passed all filters 432 return true; 433 } 434 }; 435 nodetable.sorter.setRowFilter(rf); 436 } 437 438 /*=============== Getters and Setters for core properties ===============*/ 439 440 /** 441 * @return Will closing the window quit JMRI? 442 */ 443 public boolean isAllowQuit() { 444 return allowQuit; 445 } 446 447 /** 448 * @param allowQuit Set state to either close JMRI or just the roster window 449 */ 450 public void setAllowQuit(boolean allowQuit) { 451 allowQuit(allowQuit); 452 } 453 454 /** 455 * @return the newWindowAction 456 */ 457 protected JmriAbstractAction getNewWindowAction() { 458 if (newWindowAction == null) { 459 newWindowAction = new LccProFrameAction("newWindow", this, allowQuit); 460 } 461 return newWindowAction; 462 } 463 464 /** 465 * @param newWindowAction the newWindowAction to set 466 */ 467 protected void setNewWindowAction(JmriAbstractAction newWindowAction) { 468 this.newWindowAction = newWindowAction; 469 } 470 471 @Override 472 public Object getProperty(String key) { 473 // TODO - does this have any equivalent? 474 if (key.equalsIgnoreCase("hideSummary")) { 475 return hideBottomPane; 476 } 477 // call parent getProperty method to return any properties defined 478 // in the class hierarchy. 479 return super.getProperty(key); 480 } 481 482 void handleQuit(WindowEvent e) { 483 if (e != null && frameInstances.size() == 1) { 484 final String rememberWindowClose = this.getClass().getName() + ".closeDP3prompt"; 485 if (!prefsMgr.getSimplePreferenceState(rememberWindowClose)) { 486 JPanel message = new JPanel(); 487 JLabel question = new JLabel(rb.getString("MessageLongCloseWarning")); 488 final JCheckBox remember = new JCheckBox(rb.getString("MessageRememberSetting")); 489 remember.setFont(remember.getFont().deriveFont(10.0F)); 490 message.setLayout(new BoxLayout(message, BoxLayout.Y_AXIS)); 491 message.add(question); 492 message.add(remember); 493 int result = JmriJOptionPane.showConfirmDialog(null, 494 message, 495 rb.getString("MessageShortCloseWarning"), 496 JmriJOptionPane.YES_NO_OPTION); 497 if (remember.isSelected()) { 498 prefsMgr.setSimplePreferenceState(rememberWindowClose, true); 499 } 500 if (result == JmriJOptionPane.YES_OPTION) { 501 handleQuit(); 502 } 503 } else { 504 handleQuit(); 505 } 506 } else if (frameInstances.size() > 1) { 507 final String rememberWindowClose = this.getClass().getName() + ".closeMultipleDP3prompt"; 508 if (!prefsMgr.getSimplePreferenceState(rememberWindowClose)) { 509 JPanel message = new JPanel(); 510 JLabel question = new JLabel(rb.getString("MessageLongMultipleCloseWarning")); 511 final JCheckBox remember = new JCheckBox(rb.getString("MessageRememberSetting")); 512 remember.setFont(remember.getFont().deriveFont(10.0F)); 513 message.setLayout(new BoxLayout(message, BoxLayout.Y_AXIS)); 514 message.add(question); 515 message.add(remember); 516 int result = JmriJOptionPane.showConfirmDialog(null, 517 message, 518 rb.getString("MessageShortCloseWarning"), 519 JmriJOptionPane.YES_NO_OPTION); 520 if (remember.isSelected()) { 521 prefsMgr.setSimplePreferenceState(rememberWindowClose, true); 522 } 523 if (result == JmriJOptionPane.YES_OPTION) { 524 handleQuit(); 525 } 526 } else { 527 handleQuit(); 528 } 529 //closeWindow(null); 530 } 531 } 532 533 private void handleQuit(){ 534 try { 535 InstanceManager.getDefault(jmri.ShutDownManager.class).shutdown(); 536 } catch (Exception e) { 537 log.error("Continuing after error in handleQuit", e); 538 } 539 } 540 541 protected void helpMenu(JMenuBar menuBar, final JFrame frame) { 542 // create menu and standard items 543 JMenu helpMenu = HelpUtil.makeHelpMenu("package.apps.gui3.lccpro.LccPro", true); 544 // use as main help menu 545 menuBar.add(helpMenu); 546 } 547 548 protected void hideGroups() { 549 boolean boo = !hideGroups; 550 hideGroupsPane(boo); 551 } 552 553 public void hideGroupsPane(boolean hide) { 554 if (hideGroups == hide) { 555 return; 556 } 557 hideGroups = hide; 558 if (hide) { 559 groupSplitPaneLocation = rosterGroupSplitPane.getDividerLocation(); 560 rosterGroupSplitPane.setDividerLocation(1); 561 rosterGroupSplitPane.getLeftComponent().setMinimumSize(new Dimension()); 562 } else { 563 rosterGroupSplitPane.setDividerSize(UIManager.getInt("SplitPane.dividerSize")); 564 rosterGroupSplitPane.setOneTouchExpandable(true); 565 if (groupSplitPaneLocation >= 2) { 566 rosterGroupSplitPane.setDividerLocation(groupSplitPaneLocation); 567 } else { 568 rosterGroupSplitPane.resetToPreferredSizes(); 569 } 570 } 571 } 572 573 protected void hideSummary() { 574 boolean boo = !hideBottomPane; 575 hideBottomPane(boo); 576 } 577 578 protected void newWindow() { 579 this.newWindow(this.getNewWindowAction()); 580 } 581 582 protected void newWindow(JmriAbstractAction action) { 583 action.setWindowInterface(this); 584 action.actionPerformed(null); 585 firePropertyChange("closewindow", "setEnabled", true); 586 } 587 588 /** 589 * Print the displayed table, as displayed. 590 * 591 */ 592 protected void printCurrentTable() { 593 try { 594 var cal = java.util.Calendar.getInstance(); 595 var sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm"); 596 String time = sdf.format(cal.getTime()); 597 598 String group = matchGroupName.getSelectedItem().toString(); 599 600 nodetable.getTable().print(javax.swing.JTable.PrintMode.FIT_WIDTH, 601 null, // no header 602 new java.text.MessageFormat(group+" - {0} - "+time) // spaces for heuristic formatting, don't change 603 ); 604 } catch (java.awt.print.PrinterException ep) { 605 log.error("While printing",ep); 606 } 607 } 608 609 JFileChooser fileChooser; 610 611 protected void exportCurrentTable() { 612 if (fileChooser == null) { 613 fileChooser = new jmri.util.swing.JmriJFileChooser(); 614 } 615 616 int retVal = fileChooser.showSaveDialog(this); 617 618 if (retVal == JFileChooser.APPROVE_OPTION) { 619 File file = fileChooser.getSelectedFile(); 620 if (log.isDebugEnabled()) { 621 log.debug("start to export to CSV file {}", file); 622 } 623 624 try (CSVPrinter str = new CSVPrinter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8), CSVFormat.DEFAULT)) { 625 var table = nodetable.getTable(); 626 str.printRecord( 627 table.getColumnName(LccProTableModel.NAMECOL), // done this way in case columns have been reordered 628 table.getColumnName(LccProTableModel.IDCOL), 629 table.getColumnName(LccProTableModel.MFGCOL), 630 table.getColumnName(LccProTableModel.MODELCOL), 631 table.getColumnName(LccProTableModel.SVERSIONCOL), 632 table.getColumnName(LccProTableModel.DESCRIPTIONCOL) 633 ); 634 for (int row = 0; row < table.getRowCount(); row++) { 635 var name = table.getValueAt(row, LccProTableModel.NAMECOL); 636 var id = table.getValueAt(row, LccProTableModel.IDCOL); 637 var mfg = table.getValueAt(row, LccProTableModel.MFGCOL); 638 var model = table.getValueAt(row, LccProTableModel.MODELCOL); 639 var software = table.getValueAt(row, LccProTableModel.SVERSIONCOL); 640 var description = table.getValueAt(row, LccProTableModel.DESCRIPTIONCOL); 641 642 str.printRecord(name, id, mfg, model, software, description); 643 } 644 str.flush(); 645 } catch (IOException ex) { 646 log.error("Error writing file", ex); 647 } 648 } 649 } 650 651 /** 652 * Sets the backup directory used by the Backup buttons and 653 * the NodeBackupAction class. 654 */ 655 protected void setBackupDirectory() { 656 NodeBackupAction.showOpenDialog(this); 657 } 658 659 /** 660 * Match the first argument in the array against a locally-known method. 661 * 662 * @param args Array of arguments, we take with element 0 663 */ 664 @Override 665 public void remoteCalls(String[] args) { 666 args[0] = args[0].toLowerCase(); 667 switch (args[0]) { 668 case "summarypane": 669 hideSummary(); 670 break; 671 case "groupspane": 672 hideGroups(); 673 break; 674 case "quit": 675 saveWindowDetails(); 676 handleQuit(new WindowEvent(this, frameInstances.size())); 677 break; 678 case "closewindow": 679 closeWindow(null); 680 break; 681 case "newwindow": 682 newWindow(); 683 break; 684 case "resettablecolumns": 685 nodetable.resetColumnWidths(); 686 break; 687 case "printcurrenttable": 688 printCurrentTable(); 689 break; 690 case "exportcurrenttable": 691 exportCurrentTable(); 692 break; 693 case "setbackupdirectory": 694 setBackupDirectory(); 695 break; 696 default: 697 log.error("method {} not found", args[0]); 698 break; 699 } 700 } 701 702 void saveWindowDetails() { 703 if (prefsMgr != null) { // aborted startup doesn't set prefs manager 704 prefsMgr.setSimplePreferenceState(this.getClass().getName() + ".hideSummary", hideBottomPane); 705 prefsMgr.setSimplePreferenceState(this.getClass().getName() + ".hideGroups", hideGroups); 706 if (rosterGroupSplitPane.getDividerLocation() > 2) { 707 prefsMgr.setProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation", rosterGroupSplitPane.getDividerLocation()); 708 } else if (groupSplitPaneLocation > 2) { 709 prefsMgr.setProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation", groupSplitPaneLocation); 710 } 711 } 712 } 713 714 protected void showPopup(JmriMouseEvent e) { 715 int row = nodetable.getTable().rowAtPoint(e.getPoint()); 716 if (!nodetable.getTable().isRowSelected(row)) { 717 nodetable.getTable().changeSelection(row, 0, false, false); 718 } 719 JPopupMenu popupMenu = new JPopupMenu(); 720 721 NodeID node = new NodeID((String) nodetable.getTable().getValueAt(row, LccProTableModel.IDCOL)); 722 723 var addMenu = new JMenuItem(Bundle.getMessage("FrameAddNodeToGroup")); 724 addMenu.addActionListener((ActionEvent evt) -> { 725 addToGroupPrompt(node); 726 }); 727 popupMenu.add(addMenu); 728 729 var removeMenu = new JMenuItem(Bundle.getMessage("FrameRemoveNodeFromGroup")); 730 removeMenu.addActionListener((ActionEvent evt) -> { 731 removeFromGroupPrompt(node); 732 }); 733 popupMenu.add(removeMenu); 734 735 var restartMenu = new JMenuItem(Bundle.getMessage("FrameRestartNode")); 736 restartMenu.addActionListener((ActionEvent evt) -> { 737 restart(node); 738 }); 739 popupMenu.add(restartMenu); 740 741 var clearCdiMenu = new JMenuItem(Bundle.getMessage("FrameClearCdiCache")); 742 clearCdiMenu.addActionListener((ActionEvent evt) -> { 743 clearCDI(node); 744 }); 745 popupMenu.add(clearCdiMenu); 746 747 popupMenu.show(e.getComponent(), e.getX(), e.getY()); 748 } 749 750 void addToGroupPrompt(NodeID node) { 751 var group = JmriJOptionPane.showInputDialog( 752 null, Bundle.getMessage("FrameAddToGroup"), Bundle.getMessage("FrameAddToGroupTit"), 753 JmriJOptionPane.QUESTION_MESSAGE 754 ); 755 if (! group.isEmpty()) { 756 groupStore.addNodeToGroup(node, group); 757 } 758 updateMatchGroupName(); 759 } 760 761 void removeFromGroupPrompt(NodeID node) { 762 var group = JmriJOptionPane.showInputDialog( 763 null, Bundle.getMessage("FrameRemoveFromGroup"), Bundle.getMessage("FrameRemoveFromGroupTit"), 764 JmriJOptionPane.QUESTION_MESSAGE 765 ); 766 if (! group.isEmpty()) { 767 groupStore.removeNodeFromGroup(node, group); 768 } 769 updateMatchGroupName(); 770 } 771 772 void restart(NodeID node) { 773 memo.get(OlcbInterface.class).getDatagramService() 774 .sendData(node, new int[] {0x20, 0xA9}); 775 } 776 777 void clearCDI(NodeID destNodeID) { 778 jmri.jmrix.openlcb.swing.DropCdiCache.drop(destNodeID, memo.get(OlcbInterface.class)); 779 } 780 781 /** 782 * Create and display a status bar along the bottom edge of the Roster main 783 * pane. 784 */ 785 protected void statusBar() { 786 for (ConnectionConfig conn : InstanceManager.getDefault(ConnectionConfigManager.class)) { 787 if (!conn.getDisabled()) { 788 addToStatusBox(new ConnectionLabel(conn)); 789 } 790 } 791 addToStatusBox(new TrafficStatusLabel(memo)); 792 } 793 794 protected void systemsMenu() { 795 ActiveSystemsMenu.addItems(getMenu()); 796 getMenu().add(new WindowMenu(this)); 797 } 798 799 void updateDetails() { 800 // TODO - once we decide what details to show, fix this 801 } 802 803 @Override 804 @SuppressFBWarnings(value = "OVERRIDING_METHODS_MUST_INVOKE_SUPER", 805 justification = "This calls closeWindow which invokes the super method") 806 public void windowClosing(WindowEvent e) { 807 closeWindow(e); 808 } 809 810 /** 811 * Displays a context (right-click) menu for a node row. 812 */ 813 private class NodePopupListener extends JmriMouseAdapter { 814 815 @Override 816 public void mousePressed(JmriMouseEvent e) { 817 if (e.isPopupTrigger()) { 818 showPopup(e); 819 } 820 } 821 822 @Override 823 public void mouseReleased(JmriMouseEvent e) { 824 if (e.isPopupTrigger()) { 825 showPopup(e); 826 } 827 } 828 829 @Override 830 public void mouseClicked(JmriMouseEvent e) { 831 if (e.isPopupTrigger()) { 832 showPopup(e); 833 return; 834 } 835 } 836 } 837 838 /** 839 * Displays SNIP information about a specific node 840 */ 841 private static class NodeInfoPane extends JPanel { 842 JLabel name = new JLabel(); 843 JLabel desc = new JLabel(); 844 JLabel nodeID = new JLabel(); 845 JLabel mfg = new JLabel(); 846 JLabel model = new JLabel(); 847 JLabel hardver = new JLabel(); 848 JLabel softver = new JLabel(); 849 850 public NodeInfoPane() { 851 var gbl = new jmri.util.javaworld.GridLayout2(7,2); 852 setLayout(gbl); 853 854 var a = new JLabel(Bundle.getMessage("FrameName")); 855 a.setHorizontalAlignment(SwingConstants.RIGHT); 856 add(a); 857 add(name); 858 859 a = new JLabel(Bundle.getMessage("FrameDescription")); 860 a.setHorizontalAlignment(SwingConstants.RIGHT); 861 add(a); 862 add(desc); 863 864 a = new JLabel(Bundle.getMessage("FrameNodeId")); 865 a.setHorizontalAlignment(SwingConstants.RIGHT); 866 add(a); 867 add(nodeID); 868 869 a = new JLabel(Bundle.getMessage("FrameManufacturer")); 870 a.setHorizontalAlignment(SwingConstants.RIGHT); 871 add(a); 872 add(mfg); 873 874 a = new JLabel(Bundle.getMessage("FrameModel")); 875 a.setHorizontalAlignment(SwingConstants.RIGHT); 876 add(a); 877 add(model); 878 879 a = new JLabel(Bundle.getMessage("FrameHardwareVersion")); 880 a.setHorizontalAlignment(SwingConstants.RIGHT); 881 add(a); 882 add(hardver); 883 884 a = new JLabel(Bundle.getMessage("FrameSoftwareVersion")); 885 a.setHorizontalAlignment(SwingConstants.RIGHT); 886 add(a); 887 add(softver); 888 } 889 890 public void update(MimicNodeStore.NodeMemo nodememo) { 891 var snip = nodememo.getSimpleNodeIdent(); 892 893 // update with current contents 894 name.setText(snip.getUserName()); 895 desc.setText(snip.getUserDesc()); 896 nodeID.setText(nodememo.getNodeID().toString()); 897 mfg.setText(snip.getMfgName()); 898 model.setText(snip.getModelName()); 899 hardver.setText(snip.getHardwareVersion()); 900 softver.setText(snip.getSoftwareVersion()); 901 } 902 903 } 904 905 906 /** 907 * Displays PIP information about a specific node 908 */ 909 private static class NodePipPane extends JPanel { 910 911 public NodePipPane () { 912 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); 913 add(new JLabel(Bundle.getMessage("FrameSupportedProtocols"))); 914 } 915 916 public void update(MimicNodeStore.NodeMemo nodememo) { 917 // remove existing content 918 removeAll(); 919 revalidate(); 920 repaint(); 921 // add heading 922 add(new JLabel(Bundle.getMessage("FrameSupportedProtocols"))); 923 // and display new content 924 var pip = nodememo.getProtocolIdentification(); 925 var names = pip.getProtocolNames(); 926 927 for (String name : names) { 928 // make this name a bit more human-friendly 929 final String regex = "([a-z])([A-Z])"; 930 final String replacement = "$1 $2"; 931 var formattedName = " "+name.replaceAll(regex, replacement); 932 add(new JLabel(formattedName)); 933 } 934 } 935 } 936 937 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LccProFrame.class); 938 939}