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}