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