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    static final 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                var table = nodetable.getTable();
626                str.printRecord(
627                    table.getColumnName(LccProTableModel.NAMECOL),  // done this way in case columns have been reordered
628                    table.getColumnName(LccProTableModel.IDCOL),
629                    table.getColumnName(LccProTableModel.MFGCOL),
630                    table.getColumnName(LccProTableModel.MODELCOL),
631                    table.getColumnName(LccProTableModel.SVERSIONCOL),
632                    table.getColumnName(LccProTableModel.DESCRIPTIONCOL)
633                );
634                for (int row = 0; row < table.getRowCount(); row++) {
635                    var name = table.getValueAt(row, LccProTableModel.NAMECOL);
636                    var id = table.getValueAt(row, LccProTableModel.IDCOL);
637                    var mfg = table.getValueAt(row, LccProTableModel.MFGCOL);
638                    var model = table.getValueAt(row, LccProTableModel.MODELCOL);
639                    var software = table.getValueAt(row, LccProTableModel.SVERSIONCOL);
640                    var description = table.getValueAt(row, LccProTableModel.DESCRIPTIONCOL);
641                    
642                    str.printRecord(name, id, mfg, model, software, description);
643                }
644                str.flush();
645            } catch (IOException ex) {
646                log.error("Error writing file", ex);
647            }
648        }
649    }
650
651    /**
652     * Sets the backup directory used by the Backup buttons and 
653     * the NodeBackupAction class.
654     */
655    protected void setBackupDirectory() {
656        NodeBackupAction.showOpenDialog(this);
657    }
658    
659    /**
660     * Match the first argument in the array against a locally-known method.
661     *
662     * @param args Array of arguments, we take with element 0
663     */
664    @Override
665    public void remoteCalls(String[] args) {
666        args[0] = args[0].toLowerCase();
667        switch (args[0]) {
668            case "summarypane":
669                hideSummary();
670                break;
671            case "groupspane":
672                hideGroups();
673                break;
674            case "quit":
675                saveWindowDetails();
676                handleQuit(new WindowEvent(this, frameInstances.size()));
677                break;
678            case "closewindow":
679                closeWindow(null);
680                break;
681            case "newwindow":
682                newWindow();
683                break;
684            case "resettablecolumns":
685                nodetable.resetColumnWidths();
686                break;
687            case "printcurrenttable":
688                printCurrentTable();
689                break;
690            case "exportcurrenttable":
691                exportCurrentTable();
692                break;
693            case "setbackupdirectory":
694                setBackupDirectory();
695                break;
696            default:
697                log.error("method {} not found", args[0]);
698                break;
699        }
700    }
701
702    void saveWindowDetails() {
703        if (prefsMgr != null) {  // aborted startup doesn't set prefs manager
704            prefsMgr.setSimplePreferenceState(this.getClass().getName() + ".hideSummary", hideBottomPane);
705            prefsMgr.setSimplePreferenceState(this.getClass().getName() + ".hideGroups", hideGroups);
706            if (rosterGroupSplitPane.getDividerLocation() > 2) {
707                prefsMgr.setProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation", rosterGroupSplitPane.getDividerLocation());
708            } else if (groupSplitPaneLocation > 2) {
709                prefsMgr.setProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation", groupSplitPaneLocation);
710            }
711        }
712    }
713
714    protected void showPopup(JmriMouseEvent e) {
715        int row = nodetable.getTable().rowAtPoint(e.getPoint());
716        if (!nodetable.getTable().isRowSelected(row)) {
717            nodetable.getTable().changeSelection(row, 0, false, false);
718        }
719        JPopupMenu popupMenu = new JPopupMenu();
720        
721        NodeID node = new NodeID((String) nodetable.getTable().getValueAt(row, LccProTableModel.IDCOL));
722        
723        var addMenu = new JMenuItem(Bundle.getMessage("FrameAddNodeToGroup"));
724        addMenu.addActionListener((ActionEvent evt) -> {
725            addToGroupPrompt(node);
726        });
727        popupMenu.add(addMenu);
728
729        var removeMenu = new JMenuItem(Bundle.getMessage("FrameRemoveNodeFromGroup"));
730        removeMenu.addActionListener((ActionEvent evt) -> {
731            removeFromGroupPrompt(node);
732        });
733        popupMenu.add(removeMenu);
734        
735        var restartMenu = new JMenuItem(Bundle.getMessage("FrameRestartNode"));
736        restartMenu.addActionListener((ActionEvent evt) -> {
737            restart(node);
738        });
739        popupMenu.add(restartMenu);
740        
741        var clearCdiMenu = new JMenuItem(Bundle.getMessage("FrameClearCdiCache"));
742        clearCdiMenu.addActionListener((ActionEvent evt) -> {
743            clearCDI(node);
744        });
745        popupMenu.add(clearCdiMenu);
746        
747       popupMenu.show(e.getComponent(), e.getX(), e.getY());
748    }
749
750    void addToGroupPrompt(NodeID node) {
751        var group = JmriJOptionPane.showInputDialog(
752                    null, Bundle.getMessage("FrameAddToGroup"), Bundle.getMessage("FrameAddToGroupTit"), 
753                    JmriJOptionPane.QUESTION_MESSAGE
754                );
755        if (! group.isEmpty()) {
756            groupStore.addNodeToGroup(node, group);
757        }
758        updateMatchGroupName();
759    }
760    
761    void removeFromGroupPrompt(NodeID node) {
762        var group = JmriJOptionPane.showInputDialog(
763                    null, Bundle.getMessage("FrameRemoveFromGroup"), Bundle.getMessage("FrameRemoveFromGroupTit"), 
764                    JmriJOptionPane.QUESTION_MESSAGE
765                );
766        if (! group.isEmpty()) {
767            groupStore.removeNodeFromGroup(node, group);
768        }
769        updateMatchGroupName();
770    }
771    
772    void restart(NodeID node) {
773        memo.get(OlcbInterface.class).getDatagramService()
774            .sendData(node, new int[] {0x20, 0xA9});
775    }
776    
777    void clearCDI(NodeID destNodeID) {
778        jmri.jmrix.openlcb.swing.DropCdiCache.drop(destNodeID, memo.get(OlcbInterface.class));
779    }
780    
781    /**
782     * Create and display a status bar along the bottom edge of the Roster main
783     * pane.
784     */
785    protected void statusBar() {
786        for (ConnectionConfig conn : InstanceManager.getDefault(ConnectionConfigManager.class)) {
787            if (!conn.getDisabled()) {
788                addToStatusBox(new ConnectionLabel(conn));
789            }
790        }
791        addToStatusBox(new TrafficStatusLabel(memo));        
792    }
793
794    protected void systemsMenu() {
795        ActiveSystemsMenu.addItems(getMenu());
796        getMenu().add(new WindowMenu(this));
797    }
798
799    void updateDetails() {
800        // TODO - once we decide what details to show, fix this
801    }
802
803    @Override
804    @SuppressFBWarnings(value = "OVERRIDING_METHODS_MUST_INVOKE_SUPER",
805            justification = "This calls closeWindow which invokes the super method")
806    public void windowClosing(WindowEvent e) {
807        closeWindow(e);
808    }
809
810    /**
811     * Displays a context (right-click) menu for a node row.
812     */
813    private class NodePopupListener extends JmriMouseAdapter {
814
815        @Override
816        public void mousePressed(JmriMouseEvent e) {
817            if (e.isPopupTrigger()) {
818                showPopup(e);
819            }
820        }
821
822        @Override
823        public void mouseReleased(JmriMouseEvent e) {
824            if (e.isPopupTrigger()) {
825                showPopup(e);
826            }
827        }
828
829        @Override
830        public void mouseClicked(JmriMouseEvent e) {
831            if (e.isPopupTrigger()) {
832                showPopup(e);
833                return;
834            }
835        }
836    }
837
838    /**
839     * Displays SNIP information about a specific node
840     */
841    private static class NodeInfoPane extends JPanel {
842        JLabel name = new JLabel();
843        JLabel desc = new JLabel();
844        JLabel nodeID = new JLabel();
845        JLabel mfg = new JLabel();
846        JLabel model = new JLabel();
847        JLabel hardver = new JLabel();
848        JLabel softver = new JLabel();
849        
850        public NodeInfoPane() {
851            var gbl = new jmri.util.javaworld.GridLayout2(7,2);
852            setLayout(gbl);
853            
854            var a = new JLabel(Bundle.getMessage("FrameName"));
855            a.setHorizontalAlignment(SwingConstants.RIGHT);
856            add(a);
857            add(name);
858
859            a = new JLabel(Bundle.getMessage("FrameDescription"));
860            a.setHorizontalAlignment(SwingConstants.RIGHT);
861            add(a);
862            add(desc);
863
864            a = new JLabel(Bundle.getMessage("FrameNodeId"));
865            a.setHorizontalAlignment(SwingConstants.RIGHT);
866            add(a);
867            add(nodeID);
868            
869            a = new JLabel(Bundle.getMessage("FrameManufacturer"));
870            a.setHorizontalAlignment(SwingConstants.RIGHT);
871            add(a);
872            add(mfg);
873
874            a = new JLabel(Bundle.getMessage("FrameModel"));
875            a.setHorizontalAlignment(SwingConstants.RIGHT);
876            add(a);
877            add(model);
878
879            a = new JLabel(Bundle.getMessage("FrameHardwareVersion"));
880            a.setHorizontalAlignment(SwingConstants.RIGHT);
881            add(a);
882            add(hardver);
883
884            a = new JLabel(Bundle.getMessage("FrameSoftwareVersion"));
885            a.setHorizontalAlignment(SwingConstants.RIGHT);
886            add(a);
887            add(softver);
888        }
889        
890        public void update(MimicNodeStore.NodeMemo nodememo) {
891            var snip = nodememo.getSimpleNodeIdent();
892            
893            // update with current contents
894            name.setText(snip.getUserName());
895            desc.setText(snip.getUserDesc());
896            nodeID.setText(nodememo.getNodeID().toString());
897            mfg.setText(snip.getMfgName());
898            model.setText(snip.getModelName());
899            hardver.setText(snip.getHardwareVersion());
900            softver.setText(snip.getSoftwareVersion());
901        }
902
903    }
904    
905
906    /**
907     * Displays PIP information about a specific node
908     */
909    private static class NodePipPane extends JPanel {
910        
911        public NodePipPane () {
912            setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
913            add(new JLabel(Bundle.getMessage("FrameSupportedProtocols")));
914        }
915        
916        public void update(MimicNodeStore.NodeMemo nodememo) {
917            // remove existing content
918            removeAll();
919            revalidate();
920            repaint();
921            // add heading
922            add(new JLabel(Bundle.getMessage("FrameSupportedProtocols")));
923            // and display new content
924            var pip = nodememo.getProtocolIdentification();
925            var names = pip.getProtocolNames();
926            
927            for (String name : names) {
928                // make this name a bit more human-friendly
929                final String regex = "([a-z])([A-Z])";
930                final String replacement = "$1 $2";
931                var formattedName = "   "+name.replaceAll(regex, replacement);
932                add(new JLabel(formattedName));
933            }
934        }
935    }
936    
937    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LccProFrame.class);
938
939}