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}