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 final static 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 str.printRecord("Name", "ID", "Manufacturer", "Model", "Software", "Description"); 626 var table = nodetable.getTable(); 627 for (int row = 0; row < table.getRowCount(); row++) { 628 var name = table.getValueAt(row, LccProTableModel.NAMECOL); 629 var id = table.getValueAt(row, LccProTableModel.IDCOL); 630 var mfg = table.getValueAt(row, LccProTableModel.MFGCOL); 631 var model = table.getValueAt(row, LccProTableModel.MODELCOL); 632 var software = table.getValueAt(row, LccProTableModel.SVERSIONCOL); 633 var description = table.getValueAt(row, LccProTableModel.DESCRIPTIONCOL); 634 635 str.printRecord(name, id, mfg, model, software, description); 636 } 637 str.flush(); 638 } catch (IOException ex) { 639 log.error("Error writing file", ex); 640 } 641 } 642 } 643 644 /** 645 * Sets the backup directory used by the Backup buttons and 646 * the NodeBackupAction class. 647 */ 648 protected void setBackupDirectory() { 649 NodeBackupAction.showOpenDialog(this); 650 } 651 652 /** 653 * Match the first argument in the array against a locally-known method. 654 * 655 * @param args Array of arguments, we take with element 0 656 */ 657 @Override 658 public void remoteCalls(String[] args) { 659 args[0] = args[0].toLowerCase(); 660 switch (args[0]) { 661 case "summarypane": 662 hideSummary(); 663 break; 664 case "groupspane": 665 hideGroups(); 666 break; 667 case "quit": 668 saveWindowDetails(); 669 handleQuit(new WindowEvent(this, frameInstances.size())); 670 break; 671 case "closewindow": 672 closeWindow(null); 673 break; 674 case "newwindow": 675 newWindow(); 676 break; 677 case "resettablecolumns": 678 nodetable.resetColumnWidths(); 679 break; 680 case "printcurrenttable": 681 printCurrentTable(); 682 break; 683 case "exportcurrenttable": 684 exportCurrentTable(); 685 break; 686 case "setbackupdirectory": 687 setBackupDirectory(); 688 break; 689 default: 690 log.error("method {} not found", args[0]); 691 break; 692 } 693 } 694 695 void saveWindowDetails() { 696 if (prefsMgr != null) { // aborted startup doesn't set prefs manager 697 prefsMgr.setSimplePreferenceState(this.getClass().getName() + ".hideSummary", hideBottomPane); 698 prefsMgr.setSimplePreferenceState(this.getClass().getName() + ".hideGroups", hideGroups); 699 if (rosterGroupSplitPane.getDividerLocation() > 2) { 700 prefsMgr.setProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation", rosterGroupSplitPane.getDividerLocation()); 701 } else if (groupSplitPaneLocation > 2) { 702 prefsMgr.setProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation", groupSplitPaneLocation); 703 } 704 } 705 } 706 707 protected void showPopup(JmriMouseEvent e) { 708 int row = nodetable.getTable().rowAtPoint(e.getPoint()); 709 if (!nodetable.getTable().isRowSelected(row)) { 710 nodetable.getTable().changeSelection(row, 0, false, false); 711 } 712 JPopupMenu popupMenu = new JPopupMenu(); 713 714 NodeID node = new NodeID((String) nodetable.getTable().getValueAt(row, LccProTableModel.IDCOL)); 715 716 var addMenu = new JMenuItem(Bundle.getMessage("FrameAddNodeToGroup")); 717 addMenu.addActionListener((ActionEvent evt) -> { 718 addToGroupPrompt(node); 719 }); 720 popupMenu.add(addMenu); 721 722 var removeMenu = new JMenuItem(Bundle.getMessage("FrameRemoveNodeFromGroup")); 723 removeMenu.addActionListener((ActionEvent evt) -> { 724 removeFromGroupPrompt(node); 725 }); 726 popupMenu.add(removeMenu); 727 728 var restartMenu = new JMenuItem(Bundle.getMessage("FrameRestartNode")); 729 restartMenu.addActionListener((ActionEvent evt) -> { 730 restart(node); 731 }); 732 popupMenu.add(restartMenu); 733 734 var clearCdiMenu = new JMenuItem(Bundle.getMessage("FrameClearCdiCache")); 735 clearCdiMenu.addActionListener((ActionEvent evt) -> { 736 clearCDI(node); 737 }); 738 popupMenu.add(clearCdiMenu); 739 740 popupMenu.show(e.getComponent(), e.getX(), e.getY()); 741 } 742 743 void addToGroupPrompt(NodeID node) { 744 var group = JmriJOptionPane.showInputDialog( 745 null, Bundle.getMessage("FrameAddToGroup"), Bundle.getMessage("FrameAddToGroupTit"), 746 JmriJOptionPane.QUESTION_MESSAGE 747 ); 748 if (! group.isEmpty()) { 749 groupStore.addNodeToGroup(node, group); 750 } 751 updateMatchGroupName(); 752 } 753 754 void removeFromGroupPrompt(NodeID node) { 755 var group = JmriJOptionPane.showInputDialog( 756 null, Bundle.getMessage("FrameRemoveFromGroup"), Bundle.getMessage("FrameRemoveFromGroupTit"), 757 JmriJOptionPane.QUESTION_MESSAGE 758 ); 759 if (! group.isEmpty()) { 760 groupStore.removeNodeFromGroup(node, group); 761 } 762 updateMatchGroupName(); 763 } 764 765 void restart(NodeID node) { 766 memo.get(OlcbInterface.class).getDatagramService() 767 .sendData(node, new int[] {0x20, 0xA9}); 768 } 769 770 void clearCDI(NodeID destNodeID) { 771 jmri.jmrix.openlcb.swing.DropCdiCache.drop(destNodeID, memo.get(OlcbInterface.class)); 772 } 773 774 /** 775 * Create and display a status bar along the bottom edge of the Roster main 776 * pane. 777 */ 778 protected void statusBar() { 779 for (ConnectionConfig conn : InstanceManager.getDefault(ConnectionConfigManager.class)) { 780 if (!conn.getDisabled()) { 781 addToStatusBox(new ConnectionLabel(conn)); 782 } 783 } 784 addToStatusBox(new TrafficStatusLabel(memo)); 785 } 786 787 protected void systemsMenu() { 788 ActiveSystemsMenu.addItems(getMenu()); 789 getMenu().add(new WindowMenu(this)); 790 } 791 792 void updateDetails() { 793 // TODO - once we decide what details to show, fix this 794 } 795 796 @Override 797 @SuppressFBWarnings(value = "OVERRIDING_METHODS_MUST_INVOKE_SUPER", 798 justification = "This calls closeWindow which invokes the super method") 799 public void windowClosing(WindowEvent e) { 800 closeWindow(e); 801 } 802 803 /** 804 * Displays a context (right-click) menu for a node row. 805 */ 806 private class NodePopupListener extends JmriMouseAdapter { 807 808 @Override 809 public void mousePressed(JmriMouseEvent e) { 810 if (e.isPopupTrigger()) { 811 showPopup(e); 812 } 813 } 814 815 @Override 816 public void mouseReleased(JmriMouseEvent e) { 817 if (e.isPopupTrigger()) { 818 showPopup(e); 819 } 820 } 821 822 @Override 823 public void mouseClicked(JmriMouseEvent e) { 824 if (e.isPopupTrigger()) { 825 showPopup(e); 826 return; 827 } 828 } 829 } 830 831 /** 832 * Displays SNIP information about a specific node 833 */ 834 private static class NodeInfoPane extends JPanel { 835 JLabel name = new JLabel(); 836 JLabel desc = new JLabel(); 837 JLabel nodeID = new JLabel(); 838 JLabel mfg = new JLabel(); 839 JLabel model = new JLabel(); 840 JLabel hardver = new JLabel(); 841 JLabel softver = new JLabel(); 842 843 public NodeInfoPane() { 844 var gbl = new jmri.util.javaworld.GridLayout2(7,2); 845 setLayout(gbl); 846 847 var a = new JLabel(Bundle.getMessage("FrameName")); 848 a.setHorizontalAlignment(SwingConstants.RIGHT); 849 add(a); 850 add(name); 851 852 a = new JLabel(Bundle.getMessage("FrameDescription")); 853 a.setHorizontalAlignment(SwingConstants.RIGHT); 854 add(a); 855 add(desc); 856 857 a = new JLabel(Bundle.getMessage("FrameNodeId")); 858 a.setHorizontalAlignment(SwingConstants.RIGHT); 859 add(a); 860 add(nodeID); 861 862 a = new JLabel(Bundle.getMessage("FrameManufacturer")); 863 a.setHorizontalAlignment(SwingConstants.RIGHT); 864 add(a); 865 add(mfg); 866 867 a = new JLabel(Bundle.getMessage("FrameModel")); 868 a.setHorizontalAlignment(SwingConstants.RIGHT); 869 add(a); 870 add(model); 871 872 a = new JLabel(Bundle.getMessage("FrameHardwareVersion")); 873 a.setHorizontalAlignment(SwingConstants.RIGHT); 874 add(a); 875 add(hardver); 876 877 a = new JLabel(Bundle.getMessage("FrameSoftwareVersion")); 878 a.setHorizontalAlignment(SwingConstants.RIGHT); 879 add(a); 880 add(softver); 881 } 882 883 public void update(MimicNodeStore.NodeMemo nodememo) { 884 var snip = nodememo.getSimpleNodeIdent(); 885 886 // update with current contents 887 name.setText(snip.getUserName()); 888 desc.setText(snip.getUserDesc()); 889 nodeID.setText(nodememo.getNodeID().toString()); 890 mfg.setText(snip.getMfgName()); 891 model.setText(snip.getModelName()); 892 hardver.setText(snip.getHardwareVersion()); 893 softver.setText(snip.getSoftwareVersion()); 894 } 895 896 } 897 898 899 /** 900 * Displays PIP information about a specific node 901 */ 902 private static class NodePipPane extends JPanel { 903 904 public NodePipPane () { 905 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); 906 add(new JLabel(Bundle.getMessage("FrameSupportedProtocols"))); 907 } 908 909 public void update(MimicNodeStore.NodeMemo nodememo) { 910 // remove existing content 911 removeAll(); 912 revalidate(); 913 repaint(); 914 // add heading 915 add(new JLabel(Bundle.getMessage("FrameSupportedProtocols"))); 916 // and display new content 917 var pip = nodememo.getProtocolIdentification(); 918 var names = pip.getProtocolNames(); 919 920 for (String name : names) { 921 // make this name a bit more human-friendly 922 final String regex = "([a-z])([A-Z])"; 923 final String replacement = "$1 $2"; 924 var formattedName = " "+name.replaceAll(regex, replacement); 925 add(new JLabel(formattedName)); 926 } 927 } 928 } 929 930 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LccProFrame.class); 931 932}