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}