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