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