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 popupMenu.show(e.getComponent(), e.getX(), e.getY()); 650 } 651 652 void addToGroupPrompt(NodeID node) { 653 var group = JmriJOptionPane.showInputDialog( 654 null, "Add to Group:", "Add to Group", 655 JmriJOptionPane.QUESTION_MESSAGE 656 ); 657 if (! group.isEmpty()) { 658 groupStore.addNodeToGroup(node, group); 659 } 660 updateMatchGroupName(); 661 } 662 663 void removeFromGroupPrompt(NodeID node) { 664 var group = JmriJOptionPane.showInputDialog( 665 null, "Remove from Group:", "Remove from Group", 666 JmriJOptionPane.QUESTION_MESSAGE 667 ); 668 if (! group.isEmpty()) { 669 groupStore.removeNodeFromGroup(node, group); 670 } 671 updateMatchGroupName(); 672 } 673 674 /** 675 * Create and display a status bar along the bottom edge of the Roster main 676 * pane. 677 */ 678 protected void statusBar() { 679 for (ConnectionConfig conn : InstanceManager.getDefault(ConnectionConfigManager.class)) { 680 if (!conn.getDisabled()) { 681 addToStatusBox(new ConnectionLabel(conn)); 682 } 683 } 684 addToStatusBox(new TrafficStatusLabel(memo)); 685 } 686 687 protected void systemsMenu() { 688 ActiveSystemsMenu.addItems(getMenu()); 689 getMenu().add(new WindowMenu(this)); 690 } 691 692 void updateDetails() { 693 // TODO - once we decide what details to show, fix this 694 } 695 696 @Override 697 public void windowClosing(WindowEvent e) { 698 closeWindow(e); 699 } 700 701 /** 702 * Displays a context (right-click) menu for a node row. 703 */ 704 private class NodePopupListener extends JmriMouseAdapter { 705 706 @Override 707 public void mousePressed(JmriMouseEvent e) { 708 if (e.isPopupTrigger()) { 709 showPopup(e); 710 } 711 } 712 713 @Override 714 public void mouseReleased(JmriMouseEvent e) { 715 if (e.isPopupTrigger()) { 716 showPopup(e); 717 } 718 } 719 720 @Override 721 public void mouseClicked(JmriMouseEvent e) { 722 if (e.isPopupTrigger()) { 723 showPopup(e); 724 return; 725 } 726 } 727 } 728 729 /** 730 * Displays SNIP information about a specific node 731 */ 732 private static class NodeInfoPane extends JPanel { 733 JLabel name = new JLabel(); 734 JLabel desc = new JLabel(); 735 JLabel nodeID = new JLabel(); 736 JLabel mfg = new JLabel(); 737 JLabel model = new JLabel(); 738 JLabel hardver = new JLabel(); 739 JLabel softver = new JLabel(); 740 741 public NodeInfoPane() { 742 var gbl = new jmri.util.javaworld.GridLayout2(7,2); 743 setLayout(gbl); 744 745 var a = new JLabel("Name: "); 746 a.setHorizontalAlignment(SwingConstants.RIGHT); 747 add(a); 748 add(name); 749 750 a = new JLabel("Description: "); 751 a.setHorizontalAlignment(SwingConstants.RIGHT); 752 add(a); 753 add(desc); 754 755 a = new JLabel("Node ID: "); 756 a.setHorizontalAlignment(SwingConstants.RIGHT); 757 add(a); 758 add(nodeID); 759 760 a = new JLabel("Manufacturer: "); 761 a.setHorizontalAlignment(SwingConstants.RIGHT); 762 add(a); 763 add(mfg); 764 765 a = new JLabel("Model: "); 766 a.setHorizontalAlignment(SwingConstants.RIGHT); 767 add(a); 768 add(model); 769 770 a = new JLabel("Hardware Version: "); 771 a.setHorizontalAlignment(SwingConstants.RIGHT); 772 add(a); 773 add(hardver); 774 775 a = new JLabel("Software Version: "); 776 a.setHorizontalAlignment(SwingConstants.RIGHT); 777 add(a); 778 add(softver); 779 } 780 781 public void update(MimicNodeStore.NodeMemo nodememo) { 782 var snip = nodememo.getSimpleNodeIdent(); 783 784 // update with current contents 785 name.setText(snip.getUserName()); 786 desc.setText(snip.getUserDesc()); 787 nodeID.setText(nodememo.getNodeID().toString()); 788 mfg.setText(snip.getMfgName()); 789 model.setText(snip.getModelName()); 790 hardver.setText(snip.getHardwareVersion()); 791 softver.setText(snip.getSoftwareVersion()); 792 } 793 794 } 795 796 797 /** 798 * Displays PIP information about a specific node 799 */ 800 private static class NodePipPane extends JPanel { 801 802 public NodePipPane () { 803 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); 804 add(new JLabel("Supported Protocols:")); 805 } 806 807 public void update(MimicNodeStore.NodeMemo nodememo) { 808 // remove existing content 809 removeAll(); 810 revalidate(); 811 repaint(); 812 // add heading 813 add(new JLabel("Supported Protocols:")); 814 // and display new content 815 var pip = nodememo.getProtocolIdentification(); 816 var names = pip.getProtocolNames(); 817 818 for (String name : names) { 819 // make this name a bit more human-friendly 820 final String regex = "([a-z])([A-Z])"; 821 final String replacement = "$1 $2"; 822 var formattedName = " "+name.replaceAll(regex, replacement); 823 add(new JLabel(formattedName)); 824 } 825 } 826 } 827 828 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LccProFrame.class); 829 830}