001package jmri.jmrix.openlcb.swing.lccpro;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.awt.*;
006import java.awt.event.*;
007import java.awt.datatransfer.Transferable;
008
009import java.beans.PropertyChangeEvent;
010import java.beans.PropertyChangeListener;
011import java.io.*;
012import java.nio.charset.StandardCharsets;
013import java.util.ArrayList;
014
015import javax.swing.*;
016import javax.swing.event.ListSelectionEvent;
017
018import jmri.InstanceManager;
019import jmri.ShutDownManager;
020import jmri.UserPreferencesManager;
021
022import jmri.swing.ConnectionLabel;
023import jmri.swing.JTablePersistenceManager;
024import jmri.swing.RowSorterUtil;
025
026import jmri.jmrix.ActiveSystemsMenu;
027import jmri.jmrix.ConnectionConfig;
028import jmri.jmrix.ConnectionConfigManager;
029import jmri.jmrix.can.CanSystemConnectionMemo;
030import jmri.jmrix.openlcb.OlcbNodeGroupStore;
031import jmri.jmrix.openlcb.swing.TrafficStatusLabel;
032
033import jmri.util.*;
034import jmri.util.datatransfer.RosterEntrySelection;
035import jmri.util.swing.*;
036import jmri.util.swing.multipane.TwoPaneTBWindow;
037
038import org.apache.commons.csv.CSVFormat;
039import org.apache.commons.csv.CSVPrinter;
040
041import org.openlcb.*;
042
043/**
044 * A window for LCC Network management.
045 *
046 * @author Bob Jacobsen Copyright (C) 2024
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    OlcbNodeGroupStore groupStore;
057
058    public LccProFrame(String name) {
059        this(name,
060            jmri.InstanceManager.getNullableDefault(jmri.jmrix.can.CanSystemConnectionMemo.class));
061    }
062
063    public LccProFrame(String name, CanSystemConnectionMemo memo) {
064        this(name,
065                "xml/config/parts/apps/gui3/lccpro/LccProFrameMenu.xml",
066                "xml/config/parts/apps/gui3/lccpro/LccProFrameToolBar.xml",
067                memo);
068    }
069
070    public LccProFrame(String name, String menubarFile, String toolbarFile) {
071        this(name, menubarFile, toolbarFile, jmri.InstanceManager.getNullableDefault(jmri.jmrix.can.CanSystemConnectionMemo.class));
072    }
073
074    public LccProFrame(String name, String menubarFile, String toolbarFile, CanSystemConnectionMemo memo) {
075        super(name, menubarFile, toolbarFile);
076        this.memo = memo;
077        if (memo == null) {
078            // a functional LccFrame can't be created without an LCC ConnectionConfig
079            javax.swing.JOptionPane.showMessageDialog(this, "LccPro requires a configured LCC or OpenLCB connection, will quit now",
080               "LccPro", JOptionPane.ERROR_MESSAGE);
081            // and close the program
082            // This is justified because this should never happen in a properly
083            // built application:  The existence of an LCC/OpenLCB connection
084            // should have been checked long before reaching this point.
085            InstanceManager.getDefault(jmri.ShutDownManager.class).shutdown();
086            return;
087        }
088        this.nodestore = memo.get(MimicNodeStore.class);
089        this.groupStore = InstanceManager.getDefault(OlcbNodeGroupStore.class);
090        this.allowInFrameServlet = false;
091        prefsMgr = InstanceManager.getDefault(UserPreferencesManager.class);
092        this.setTitle(name);
093        this.buildWindow();
094    }
095
096    final NodeInfoPane nodeInfoPane = new NodeInfoPane();
097    final NodePipPane nodePipPane = new NodePipPane();
098    JLabel firstHelpLabel;
099    int groupSplitPaneLocation = 0;
100    boolean hideGroups = false;
101    final JTextPane id = new JTextPane();
102    UserPreferencesManager prefsMgr;
103    final java.util.ResourceBundle rb = java.util.ResourceBundle.getBundle("apps.AppsBundle");
104    // the three parts of the bottom half
105    final JPanel bottomPanel = new JPanel();
106    JSplitPane bottomLCPanel;  // left and center parts
107    JSplitPane bottomRPanel;  // right part
108    // main center window (TODO: rename this; TODO: Does this still need to be split?)
109    JSplitPane rosterGroupSplitPane;
110    
111    LccProTable nodetable;   // node table in center of screen   
112
113    JComboBox<String> matchGroupName;   // required group name to display; index <= 0 is all
114
115    final JLabel statusField = new JLabel();
116    final static Dimension summaryPaneDim = new Dimension(0, 170);
117
118    protected void additionsToToolBar() {
119        getToolBar().add(Box.createHorizontalGlue());
120    }
121
122    /**
123     * For use when the DP3 window is called from another JMRI instance, set
124     * this to prevent the DP3 from shutting down JMRI when the window is
125     * closed.
126     *
127     * @param quitAllowed true if closing window should quit application; false
128     *                    otherwise
129     */
130    protected void allowQuit(boolean quitAllowed) {
131        if (allowQuit != quitAllowed) {
132            newWindowAction = null;
133            allowQuit = quitAllowed;
134        }
135
136        firePropertyChange("quit", "setEnabled", allowQuit);
137        //if we are not allowing quit, ie opened from JMRI classic
138        //then we must at least allow the window to be closed
139        if (!allowQuit) {
140            firePropertyChange("closewindow", "setEnabled", true);
141        }
142    }
143    
144    // Create right side of the bottom panel
145
146    JPanel bottomRight() {
147        JPanel panel = new JPanel();
148        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
149        panel.setAlignmentX(SwingConstants.LEFT);
150
151        var searchPanel = new JPanel();
152        searchPanel.setLayout(new WrapLayout());
153        searchPanel.add(new JLabel(Bundle.getMessage("FrameSearchNodeNames")));
154        var searchField = new JTextField(12) {
155            @Override
156            public Dimension getMaximumSize() {
157                Dimension size = super.getMaximumSize();
158                size.height = getPreferredSize().height;
159                return size;
160            } 
161        };
162        searchField.getDocument().putProperty("filterNewlines", Boolean.TRUE);
163        searchField.addKeyListener(new KeyListener() {
164            @Override
165            public void keyTyped(KeyEvent keyEvent) {
166           }
167
168            @Override
169            public void keyReleased(KeyEvent keyEvent) {
170                // on release so the searchField has been updated
171                log.debug("keyTyped {} content {}", keyEvent.getKeyCode(), searchField.getText());
172                String search = searchField.getText().toLowerCase();
173                // start search process
174                int count = nodetable.getModel().getRowCount();
175                for (int row = 0; row < count; row++) {
176                    String value = ((String)nodetable.getTable().getValueAt(row, LccProTableModel.NAMECOL)).toLowerCase();
177                    if (value.startsWith(search)) {
178                        log.trace("  Hit value {} on {}", value, row);
179                        nodetable.getTable().setRowSelectionInterval(row, row);
180                        nodetable.getTable().scrollRectToVisible(nodetable.getTable().getCellRect(row,LccProTableModel.NAMECOL, true)); 
181                        return;
182                    }
183                }
184                // here we didn't find anything
185                nodetable.getTable().clearSelection();
186            }
187
188            @Override
189            public void keyPressed(KeyEvent keyEvent) {
190            }
191        });
192        searchPanel.add(searchField);
193        panel.add(searchPanel);
194        
195        
196        var groupPanel = new JPanel();
197        groupPanel.setLayout(new WrapLayout());
198        JLabel display = new JLabel(Bundle.getMessage("FrameDisplayNodeGroups"));
199        display.setToolTipText(Bundle.getMessage("FrameDisplayNodeGroupsTt"));
200        groupPanel.add(display);
201        
202        matchGroupName = new JComboBox<>();
203        updateMatchGroupName();     // before adding listener
204        matchGroupName.addActionListener((ActionEvent e) -> {
205            filter();
206        });
207        groupStore.addPropertyChangeListener((PropertyChangeEvent evt) -> {
208            updateMatchGroupName();
209        });
210        groupPanel.add(matchGroupName);
211        panel.add(groupPanel);
212        
213        panel.add(Box.createVerticalGlue());
214        
215        return panel;
216    }
217
218    // load updateMatchGroup combobox with current contents
219    protected void updateMatchGroupName() {
220        matchGroupName.removeAllItems();
221        matchGroupName.addItem(Bundle.getMessage("FrameAllGroups"));
222        
223        var list = groupStore.getGroupNames();
224        for (String group : list) {
225            matchGroupName.addItem(group);
226        }        
227    }
228
229    protected final void buildWindow() {
230        //Additions to the toolbar need to be added first otherwise when trying to hide bits up during the initialisation they remain on screen
231        additionsToToolBar();
232        frameInstances.add(this);
233        getTop().add(createTop());
234        getBottom().setMinimumSize(summaryPaneDim);
235        getBottom().add(createBottom());
236        statusBar();
237        systemsMenu();
238        helpMenu(getMenu(), this);
239
240        if (prefsMgr.getSimplePreferenceState(this.getClass().getName() + ".hideSummary")) {
241            //We have to set it to display first, then we can hide it.
242            hideBottomPane(false);
243            hideBottomPane(true);
244        }
245        PropertyChangeListener propertyChangeListener = (PropertyChangeEvent changeEvent) -> {
246            JSplitPane sourceSplitPane = (JSplitPane) changeEvent.getSource();
247            String propertyName = changeEvent.getPropertyName();
248            if (propertyName.equals(JSplitPane.LAST_DIVIDER_LOCATION_PROPERTY)) {
249                int current = sourceSplitPane.getDividerLocation() + sourceSplitPane.getDividerSize();
250                int panesize = (int) (sourceSplitPane.getSize().getHeight());
251                hideBottomPane = panesize - current <= 1;
252                //p.setSimplePreferenceState(DecoderPro3Window.class.getName()+".hideSummary",hideSummary);
253            }
254        };
255
256        getSplitPane().addPropertyChangeListener(propertyChangeListener);
257        if (frameInstances.size() > 1) {
258            firePropertyChange("closewindow", "setEnabled", true);
259            allowQuit(frameInstances.get(0).isAllowQuit());
260        } else {
261            firePropertyChange("closewindow", "setEnabled", false);
262        }
263    }
264
265    //@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
266    void closeWindow(WindowEvent e) {
267        saveWindowDetails();
268        if (allowQuit && frameInstances.size() == 1 && !InstanceManager.getDefault(ShutDownManager.class).isShuttingDown()) {
269            handleQuit(e);
270        } else {
271            //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
272            frameInstances.remove(this);
273            super.windowClosing(e);
274            if ((frameInstances.size() == 1) && (allowQuit)) {
275                frameInstances.get(0).firePropertyChange("closewindow", "setEnabled", false);
276            }
277            dispose();
278        }
279    }
280
281    JComponent createBottom() {
282        JPanel leftPanel = nodeInfoPane;
283        JPanel centerPanel = nodePipPane;
284        JPanel rightPanel = bottomRight();
285                
286        bottomLCPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, centerPanel);
287        bottomRPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, bottomLCPanel, rightPanel);
288
289        leftPanel.setBorder(BorderFactory.createLineBorder(Color.black));
290        centerPanel.setBorder(BorderFactory.createLineBorder(Color.black));
291        bottomLCPanel.setBorder(null);
292        
293        bottomLCPanel.setResizeWeight(0.67);  // determined empirically
294        bottomRPanel.setResizeWeight(0.75);
295        
296        bottomLCPanel.setOneTouchExpandable(true);
297        bottomRPanel.setOneTouchExpandable(true);
298        
299        // load split locations from preferences
300        Object w = prefsMgr.getProperty(getWindowFrameRef(), "bottomLCPanelDividerLocation");
301        if (w != null) {
302            var splitPaneLocation = (Integer) w;
303            bottomLCPanel.setDividerLocation(splitPaneLocation);
304        }
305        w = prefsMgr.getProperty(getWindowFrameRef(), "bottomRPanelDividerLocation");
306        if (w != null) {
307            var splitPaneLocation = (Integer) w;
308            bottomRPanel.setDividerLocation(splitPaneLocation);
309        }
310
311        // add listeners that will store location preferences
312        bottomLCPanel.addPropertyChangeListener((PropertyChangeEvent changeEvent) -> {
313            String propertyName = changeEvent.getPropertyName();
314            if (propertyName.equals("dividerLocation")) {
315                prefsMgr.setProperty(getWindowFrameRef(), "bottomLCPanelDividerLocation", bottomLCPanel.getDividerLocation());
316            }
317        });
318        bottomRPanel.addPropertyChangeListener((PropertyChangeEvent changeEvent) -> {
319            String propertyName = changeEvent.getPropertyName();
320            if (propertyName.equals("dividerLocation")) {
321                prefsMgr.setProperty(getWindowFrameRef(), "bottomRPanelDividerLocation", bottomRPanel.getDividerLocation());
322            }
323        });
324        return bottomRPanel;
325    }
326
327    JComponent createTop() {
328        final JPanel rosters = new JPanel();
329        rosters.setLayout(new BorderLayout());
330        // set up node table
331        nodetable = new LccProTable(memo);
332        rosters.add(nodetable, BorderLayout.CENTER);
333         // add selection listener to display selected row
334        nodetable.getTable().getSelectionModel().addListSelectionListener((ListSelectionEvent e) -> {
335            JTable table = nodetable.getTable();
336            if (!e.getValueIsAdjusting()) {       
337                if (table.getSelectedRow() >= 0) {
338                    int row = table.convertRowIndexToModel(table.getSelectedRow());
339                    log.debug("Selected: {}", row);
340                    MimicNodeStore.NodeMemo nodememo = nodestore.getNodeMemos().toArray(new MimicNodeStore.NodeMemo[0])[row];
341                    log.trace("   node: {}", nodememo.getNodeID().toString());
342                    nodeInfoPane.update(nodememo);
343                    nodePipPane.update(nodememo);
344                }      
345            }
346        });
347 
348        // Set all the sort and width details of the table first.
349        String nodetableref = getWindowFrameRef() + ":nodes";
350        nodetable.getTable().setName(nodetableref);
351
352        // Allow only one column to be sorted at a time -
353        // Java allows multiple column sorting, but to effectively persist that, we
354        // need to be intelligent about which columns can be meaningfully sorted
355        // with other columns; this bypasses the problem by only allowing the
356        // last column sorted to affect sorting
357        RowSorterUtil.addSingleSortableColumnListener(nodetable.getTable().getRowSorter());
358
359        // Reset and then persist the table's ui state
360        JTablePersistenceManager tpm = InstanceManager.getNullableDefault(JTablePersistenceManager.class);
361        if (tpm != null) {
362            tpm.resetState(nodetable.getTable());
363            tpm.persist(nodetable.getTable());
364        }
365        nodetable.getTable().setDragEnabled(true);
366        nodetable.getTable().setTransferHandler(new TransferHandler() {
367
368            @Override
369            public int getSourceActions(JComponent c) {
370                return TransferHandler.COPY;
371            }
372
373            @Override
374            public Transferable createTransferable(JComponent c) {
375                JTable table = nodetable.getTable();
376                ArrayList<String> Ids = new ArrayList<>(table.getSelectedRowCount());
377                for (int i = 0; i < table.getSelectedRowCount(); i++) {
378                    // TODO replace this with something about the nodes to be dragged and dropped
379                    // Ids.add(nodetable.getModel().getValueAt(table.getRowSorter().convertRowIndexToModel(table.getSelectedRows()[i]), RostenodetableModel.IDCOL).toString());
380                }
381                return new RosterEntrySelection(Ids);
382            }
383
384            @Override
385            public void exportDone(JComponent c, Transferable t, int action) {
386                // nothing to do
387            }
388        });
389        nodetable.getTable().addMouseListener(JmriMouseListener.adapt(new NodePopupListener()));
390
391        // assemble roster/groups splitpane
392        // TODO - figure out what to do with the left side of this and expand the following
393        JPanel leftSide = new JPanel();
394        leftSide.setEnabled(false);
395        leftSide.setVisible(false);
396        
397        rosterGroupSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftSide, rosters);
398        rosterGroupSplitPane.setOneTouchExpandable(false); // TODO set this true once the leftSide is in use
399        rosterGroupSplitPane.setResizeWeight(0); // emphasize right side (nodes)
400        
401        Object w = prefsMgr.getProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation");
402        if (w != null) {
403            groupSplitPaneLocation = (Integer) w;
404            rosterGroupSplitPane.setDividerLocation(groupSplitPaneLocation);
405        }
406        
407        log.trace("createTop returns {}", rosterGroupSplitPane);
408        return rosterGroupSplitPane;
409    }
410
411    /**
412     * Set up filtering of displayed rows by group level
413     */
414    private void filter() {
415        RowFilter<LccProTableModel, Integer> rf = new RowFilter<LccProTableModel, Integer>() {
416            /**
417             * @return true if row is to be displayed
418             */
419            @Override
420            public boolean include(RowFilter.Entry<? extends LccProTableModel, ? extends Integer> entry) {
421
422                // check for group match
423                if ( matchGroupName.getSelectedIndex() > 0) {  // -1 is empty combobox
424                    String group = matchGroupName.getSelectedItem().toString();
425                    NodeID node = new NodeID((String)entry.getValue(LccProTableModel.IDCOL));
426                    if ( ! groupStore.isNodeInGroup(node, group)) {
427                            return false;
428                    }
429                }
430                
431                // passed all filters
432                return true;
433            }
434        };
435        nodetable.sorter.setRowFilter(rf);
436    }
437
438    /*=============== Getters and Setters for core properties ===============*/
439
440    /**
441     * @return Will closing the window quit JMRI?
442     */
443    public boolean isAllowQuit() {
444        return allowQuit;
445    }
446
447    /**
448     * @param allowQuit Set state to either close JMRI or just the roster window
449     */
450    public void setAllowQuit(boolean allowQuit) {
451        allowQuit(allowQuit);
452    }
453
454    /**
455     * @return the newWindowAction
456     */
457    protected JmriAbstractAction getNewWindowAction() {
458        if (newWindowAction == null) {
459            newWindowAction = new LccProFrameAction("newWindow", this, allowQuit);
460        }
461        return newWindowAction;
462    }
463
464    /**
465     * @param newWindowAction the newWindowAction to set
466     */
467    protected void setNewWindowAction(JmriAbstractAction newWindowAction) {
468        this.newWindowAction = newWindowAction;
469    }
470
471    @Override
472    public Object getProperty(String key) {
473        // TODO - does this have any equivalent?
474        if (key.equalsIgnoreCase("hideSummary")) {
475            return hideBottomPane;
476        }
477        // call parent getProperty method to return any properties defined
478        // in the class hierarchy.
479        return super.getProperty(key);
480    }
481
482    void handleQuit(WindowEvent e) {
483        if (e != null && frameInstances.size() == 1) {
484            final String rememberWindowClose = this.getClass().getName() + ".closeDP3prompt";
485            if (!prefsMgr.getSimplePreferenceState(rememberWindowClose)) {
486                JPanel message = new JPanel();
487                JLabel question = new JLabel(rb.getString("MessageLongCloseWarning"));
488                final JCheckBox remember = new JCheckBox(rb.getString("MessageRememberSetting"));
489                remember.setFont(remember.getFont().deriveFont(10.0F));
490                message.setLayout(new BoxLayout(message, BoxLayout.Y_AXIS));
491                message.add(question);
492                message.add(remember);
493                int result = JmriJOptionPane.showConfirmDialog(null,
494                        message,
495                        rb.getString("MessageShortCloseWarning"),
496                        JmriJOptionPane.YES_NO_OPTION);
497                if (remember.isSelected()) {
498                    prefsMgr.setSimplePreferenceState(rememberWindowClose, true);
499                }
500                if (result == JmriJOptionPane.YES_OPTION) {
501                    handleQuit();
502                }
503            } else {
504                handleQuit();
505            }
506        } else if (frameInstances.size() > 1) {
507            final String rememberWindowClose = this.getClass().getName() + ".closeMultipleDP3prompt";
508            if (!prefsMgr.getSimplePreferenceState(rememberWindowClose)) {
509                JPanel message = new JPanel();
510                JLabel question = new JLabel(rb.getString("MessageLongMultipleCloseWarning"));
511                final JCheckBox remember = new JCheckBox(rb.getString("MessageRememberSetting"));
512                remember.setFont(remember.getFont().deriveFont(10.0F));
513                message.setLayout(new BoxLayout(message, BoxLayout.Y_AXIS));
514                message.add(question);
515                message.add(remember);
516                int result = JmriJOptionPane.showConfirmDialog(null,
517                        message,
518                        rb.getString("MessageShortCloseWarning"),
519                        JmriJOptionPane.YES_NO_OPTION);
520                if (remember.isSelected()) {
521                    prefsMgr.setSimplePreferenceState(rememberWindowClose, true);
522                }
523                if (result == JmriJOptionPane.YES_OPTION) {
524                    handleQuit();
525                }
526            } else {
527                handleQuit();
528            }
529            //closeWindow(null);
530        }
531    }
532
533    private void handleQuit(){
534        try {
535            InstanceManager.getDefault(jmri.ShutDownManager.class).shutdown();
536        } catch (Exception e) {
537            log.error("Continuing after error in handleQuit", e);
538        }
539    }
540
541    protected void helpMenu(JMenuBar menuBar, final JFrame frame) {
542        // create menu and standard items
543        JMenu helpMenu = HelpUtil.makeHelpMenu("package.apps.gui3.lccpro.LccPro", true);
544        // use as main help menu
545        menuBar.add(helpMenu);
546    }
547
548    protected void hideGroups() {
549        boolean boo = !hideGroups;
550        hideGroupsPane(boo);
551    }
552
553    public void hideGroupsPane(boolean hide) {
554        if (hideGroups == hide) {
555            return;
556        }
557        hideGroups = hide;
558        if (hide) {
559            groupSplitPaneLocation = rosterGroupSplitPane.getDividerLocation();
560            rosterGroupSplitPane.setDividerLocation(1);
561            rosterGroupSplitPane.getLeftComponent().setMinimumSize(new Dimension());
562        } else {
563            rosterGroupSplitPane.setDividerSize(UIManager.getInt("SplitPane.dividerSize"));
564            rosterGroupSplitPane.setOneTouchExpandable(true);
565            if (groupSplitPaneLocation >= 2) {
566                rosterGroupSplitPane.setDividerLocation(groupSplitPaneLocation);
567            } else {
568                rosterGroupSplitPane.resetToPreferredSizes();
569            }
570        }
571    }
572
573    protected void hideSummary() {
574        boolean boo = !hideBottomPane;
575        hideBottomPane(boo);
576    }
577
578    protected void newWindow() {
579        this.newWindow(this.getNewWindowAction());
580    }
581
582    protected void newWindow(JmriAbstractAction action) {
583        action.setWindowInterface(this);
584        action.actionPerformed(null);
585        firePropertyChange("closewindow", "setEnabled", true);
586    }
587    
588    /**
589     * Print the displayed table, as displayed.
590     *
591     */
592    protected void printCurrentTable() {
593        try {
594            var cal = java.util.Calendar.getInstance();
595            var sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm");
596            String time = sdf.format(cal.getTime());
597
598            String group = matchGroupName.getSelectedItem().toString();
599
600            nodetable.getTable().print(javax.swing.JTable.PrintMode.FIT_WIDTH,
601                            null,  // no header
602                            new java.text.MessageFormat(group+"    - {0} -   "+time)  // spaces for heuristic formatting, don't change
603                            );
604        } catch (java.awt.print.PrinterException ep) {
605            log.error("While printing",ep);
606        }    
607    }
608
609    JFileChooser fileChooser;
610    
611    protected void exportCurrentTable() {
612        if (fileChooser == null) {
613            fileChooser = new jmri.util.swing.JmriJFileChooser();
614        }
615
616        int retVal = fileChooser.showSaveDialog(this);
617
618        if (retVal == JFileChooser.APPROVE_OPTION) {
619            File file = fileChooser.getSelectedFile();
620            if (log.isDebugEnabled()) {
621                log.debug("start to export to CSV file {}", file);
622            }
623
624            try (CSVPrinter str = new CSVPrinter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8), CSVFormat.DEFAULT)) {
625                str.printRecord("Name", "ID", "Manufacturer", "Model", "Software", "Description");
626                var table = nodetable.getTable();
627                for (int row = 0; row < table.getRowCount(); row++) {
628                    var name = table.getValueAt(row, LccProTableModel.NAMECOL);
629                    var id = table.getValueAt(row, LccProTableModel.IDCOL);
630                    var mfg = table.getValueAt(row, LccProTableModel.MFGCOL);
631                    var model = table.getValueAt(row, LccProTableModel.MODELCOL);
632                    var software = table.getValueAt(row, LccProTableModel.SVERSIONCOL);
633                    var description = table.getValueAt(row, LccProTableModel.DESCRIPTIONCOL);
634                    
635                    str.printRecord(name, id, mfg, model, software, description);
636                }
637                str.flush();
638            } catch (IOException ex) {
639                log.error("Error writing file", ex);
640            }
641        }
642    }
643
644    /**
645     * Sets the backup directory used by the Backup buttons and 
646     * the NodeBackupAction class.
647     */
648    protected void setBackupDirectory() {
649        NodeBackupAction.showOpenDialog(this);
650    }
651    
652    /**
653     * Match the first argument in the array against a locally-known method.
654     *
655     * @param args Array of arguments, we take with element 0
656     */
657    @Override
658    public void remoteCalls(String[] args) {
659        args[0] = args[0].toLowerCase();
660        switch (args[0]) {
661            case "summarypane":
662                hideSummary();
663                break;
664            case "groupspane":
665                hideGroups();
666                break;
667            case "quit":
668                saveWindowDetails();
669                handleQuit(new WindowEvent(this, frameInstances.size()));
670                break;
671            case "closewindow":
672                closeWindow(null);
673                break;
674            case "newwindow":
675                newWindow();
676                break;
677            case "resettablecolumns":
678                nodetable.resetColumnWidths();
679                break;
680            case "printcurrenttable":
681                printCurrentTable();
682                break;
683            case "exportcurrenttable":
684                exportCurrentTable();
685                break;
686            case "setbackupdirectory":
687                setBackupDirectory();
688                break;
689            default:
690                log.error("method {} not found", args[0]);
691                break;
692        }
693    }
694
695    void saveWindowDetails() {
696        if (prefsMgr != null) {  // aborted startup doesn't set prefs manager
697            prefsMgr.setSimplePreferenceState(this.getClass().getName() + ".hideSummary", hideBottomPane);
698            prefsMgr.setSimplePreferenceState(this.getClass().getName() + ".hideGroups", hideGroups);
699            if (rosterGroupSplitPane.getDividerLocation() > 2) {
700                prefsMgr.setProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation", rosterGroupSplitPane.getDividerLocation());
701            } else if (groupSplitPaneLocation > 2) {
702                prefsMgr.setProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation", groupSplitPaneLocation);
703            }
704        }
705    }
706
707    protected void showPopup(JmriMouseEvent e) {
708        int row = nodetable.getTable().rowAtPoint(e.getPoint());
709        if (!nodetable.getTable().isRowSelected(row)) {
710            nodetable.getTable().changeSelection(row, 0, false, false);
711        }
712        JPopupMenu popupMenu = new JPopupMenu();
713        
714        NodeID node = new NodeID((String) nodetable.getTable().getValueAt(row, LccProTableModel.IDCOL));
715        
716        var addMenu = new JMenuItem(Bundle.getMessage("FrameAddNodeToGroup"));
717        addMenu.addActionListener((ActionEvent evt) -> {
718            addToGroupPrompt(node);
719        });
720        popupMenu.add(addMenu);
721
722        var removeMenu = new JMenuItem(Bundle.getMessage("FrameRemoveNodeFromGroup"));
723        removeMenu.addActionListener((ActionEvent evt) -> {
724            removeFromGroupPrompt(node);
725        });
726        popupMenu.add(removeMenu);
727        
728        var restartMenu = new JMenuItem(Bundle.getMessage("FrameRestartNode"));
729        restartMenu.addActionListener((ActionEvent evt) -> {
730            restart(node);
731        });
732        popupMenu.add(restartMenu);
733        
734        var clearCdiMenu = new JMenuItem(Bundle.getMessage("FrameClearCdiCache"));
735        clearCdiMenu.addActionListener((ActionEvent evt) -> {
736            clearCDI(node);
737        });
738        popupMenu.add(clearCdiMenu);
739        
740       popupMenu.show(e.getComponent(), e.getX(), e.getY());
741    }
742
743    void addToGroupPrompt(NodeID node) {
744        var group = JmriJOptionPane.showInputDialog(
745                    null, Bundle.getMessage("FrameAddToGroup"), Bundle.getMessage("FrameAddToGroupTit"), 
746                    JmriJOptionPane.QUESTION_MESSAGE
747                );
748        if (! group.isEmpty()) {
749            groupStore.addNodeToGroup(node, group);
750        }
751        updateMatchGroupName();
752    }
753    
754    void removeFromGroupPrompt(NodeID node) {
755        var group = JmriJOptionPane.showInputDialog(
756                    null, Bundle.getMessage("FrameRemoveFromGroup"), Bundle.getMessage("FrameRemoveFromGroupTit"), 
757                    JmriJOptionPane.QUESTION_MESSAGE
758                );
759        if (! group.isEmpty()) {
760            groupStore.removeNodeFromGroup(node, group);
761        }
762        updateMatchGroupName();
763    }
764    
765    void restart(NodeID node) {
766        memo.get(OlcbInterface.class).getDatagramService()
767            .sendData(node, new int[] {0x20, 0xA9});
768    }
769    
770    void clearCDI(NodeID destNodeID) {
771        jmri.jmrix.openlcb.swing.DropCdiCache.drop(destNodeID, memo.get(OlcbInterface.class));
772    }
773    
774    /**
775     * Create and display a status bar along the bottom edge of the Roster main
776     * pane.
777     */
778    protected void statusBar() {
779        for (ConnectionConfig conn : InstanceManager.getDefault(ConnectionConfigManager.class)) {
780            if (!conn.getDisabled()) {
781                addToStatusBox(new ConnectionLabel(conn));
782            }
783        }
784        addToStatusBox(new TrafficStatusLabel(memo));        
785    }
786
787    protected void systemsMenu() {
788        ActiveSystemsMenu.addItems(getMenu());
789        getMenu().add(new WindowMenu(this));
790    }
791
792    void updateDetails() {
793        // TODO - once we decide what details to show, fix this
794    }
795
796    @Override
797    @SuppressFBWarnings(value = "OVERRIDING_METHODS_MUST_INVOKE_SUPER",
798            justification = "This calls closeWindow which invokes the super method")
799    public void windowClosing(WindowEvent e) {
800        closeWindow(e);
801    }
802
803    /**
804     * Displays a context (right-click) menu for a node row.
805     */
806    private class NodePopupListener extends JmriMouseAdapter {
807
808        @Override
809        public void mousePressed(JmriMouseEvent e) {
810            if (e.isPopupTrigger()) {
811                showPopup(e);
812            }
813        }
814
815        @Override
816        public void mouseReleased(JmriMouseEvent e) {
817            if (e.isPopupTrigger()) {
818                showPopup(e);
819            }
820        }
821
822        @Override
823        public void mouseClicked(JmriMouseEvent e) {
824            if (e.isPopupTrigger()) {
825                showPopup(e);
826                return;
827            }
828        }
829    }
830
831    /**
832     * Displays SNIP information about a specific node
833     */
834    private static class NodeInfoPane extends JPanel {
835        JLabel name = new JLabel();
836        JLabel desc = new JLabel();
837        JLabel nodeID = new JLabel();
838        JLabel mfg = new JLabel();
839        JLabel model = new JLabel();
840        JLabel hardver = new JLabel();
841        JLabel softver = new JLabel();
842        
843        public NodeInfoPane() {
844            var gbl = new jmri.util.javaworld.GridLayout2(7,2);
845            setLayout(gbl);
846            
847            var a = new JLabel(Bundle.getMessage("FrameName"));
848            a.setHorizontalAlignment(SwingConstants.RIGHT);
849            add(a);
850            add(name);
851
852            a = new JLabel(Bundle.getMessage("FrameDescription"));
853            a.setHorizontalAlignment(SwingConstants.RIGHT);
854            add(a);
855            add(desc);
856
857            a = new JLabel(Bundle.getMessage("FrameNodeId"));
858            a.setHorizontalAlignment(SwingConstants.RIGHT);
859            add(a);
860            add(nodeID);
861            
862            a = new JLabel(Bundle.getMessage("FrameManufacturer"));
863            a.setHorizontalAlignment(SwingConstants.RIGHT);
864            add(a);
865            add(mfg);
866
867            a = new JLabel(Bundle.getMessage("FrameModel"));
868            a.setHorizontalAlignment(SwingConstants.RIGHT);
869            add(a);
870            add(model);
871
872            a = new JLabel(Bundle.getMessage("FrameHardwareVersion"));
873            a.setHorizontalAlignment(SwingConstants.RIGHT);
874            add(a);
875            add(hardver);
876
877            a = new JLabel(Bundle.getMessage("FrameSoftwareVersion"));
878            a.setHorizontalAlignment(SwingConstants.RIGHT);
879            add(a);
880            add(softver);
881        }
882        
883        public void update(MimicNodeStore.NodeMemo nodememo) {
884            var snip = nodememo.getSimpleNodeIdent();
885            
886            // update with current contents
887            name.setText(snip.getUserName());
888            desc.setText(snip.getUserDesc());
889            nodeID.setText(nodememo.getNodeID().toString());
890            mfg.setText(snip.getMfgName());
891            model.setText(snip.getModelName());
892            hardver.setText(snip.getHardwareVersion());
893            softver.setText(snip.getSoftwareVersion());
894        }
895
896    }
897    
898
899    /**
900     * Displays PIP information about a specific node
901     */
902    private static class NodePipPane extends JPanel {
903        
904        public NodePipPane () {
905            setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
906            add(new JLabel(Bundle.getMessage("FrameSupportedProtocols")));
907        }
908        
909        public void update(MimicNodeStore.NodeMemo nodememo) {
910            // remove existing content
911            removeAll();
912            revalidate();
913            repaint();
914            // add heading
915            add(new JLabel(Bundle.getMessage("FrameSupportedProtocols")));
916            // and display new content
917            var pip = nodememo.getProtocolIdentification();
918            var names = pip.getProtocolNames();
919            
920            for (String name : names) {
921                // make this name a bit more human-friendly
922                final String regex = "([a-z])([A-Z])";
923                final String replacement = "$1 $2";
924                var formattedName = "   "+name.replaceAll(regex, replacement);
925                add(new JLabel(formattedName));
926            }
927        }
928    }
929    
930    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LccProFrame.class);
931
932}