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