001package jmri.jmrix.can.cbus.swing.nodeconfig;
002
003import java.awt.BorderLayout;
004import java.awt.datatransfer.DataFlavor;
005import java.awt.datatransfer.Transferable;
006import java.awt.Dimension;
007import java.awt.datatransfer.UnsupportedFlavorException;
008import java.awt.event.ActionListener;
009import java.beans.PropertyChangeListener;
010import java.beans.PropertyChangeEvent;
011import java.io.IOException;
012import java.util.*;
013
014import javax.annotation.CheckForNull;
015import javax.annotation.Nonnull;
016import javax.swing.*;
017import javax.swing.event.*;
018
019import jmri.GlobalProgrammerManager;
020import jmri.jmrix.can.CanMessage;
021import jmri.jmrix.can.CanSystemConnectionMemo;
022import jmri.jmrix.can.cbus.*;
023import jmri.jmrix.can.cbus.node.CbusNode;
024import jmri.jmrix.can.cbus.node.CbusNodeEvent;
025import jmri.jmrix.can.cbus.node.CbusNodeTableDataModel;
026import jmri.jmrix.can.cbus.swing.modules.CbusConfigPaneProvider;
027import jmri.util.ThreadingUtil;
028import jmri.util.swing.JmriJOptionPane;
029
030/**
031 * Master Pane for CBUS node configuration incl. CBUS node table
032 *
033 * @author Bob Jacobsen Copyright (C) 2008
034 * @author Steve Young (C) 2019
035 * @see CbusNodeTableDataModel
036 *
037 * @since 2.99.2
038 */
039public class NodeConfigToolPane extends jmri.jmrix.can.swing.CanPanel implements PropertyChangeListener{
040
041    public JTable nodeTable;
042    private CbusPreferences preferences;
043
044    protected CbusNodeTablePane nodeTablePane;
045    private CbusNodeRestoreFcuFrame fcuFrame;
046    private CbusNodeEditEventFrame _editEventFrame;
047
048    private JScrollPane eventScroll;
049    private JSplitPane split;
050    protected JTabbedPane tabbedPane;
051
052    private ArrayList<CbusNodeConfigTab> tabbedPanes;
053
054    private int _selectedNode;
055    private jmri.util.swing.BusyDialog busy_dialog;
056
057    public int NODE_SEARCH_TIMEOUT = 5000;
058
059    private final JMenuItem teachNodeFromFcuFile;
060    private final JMenuItem searchForNodesMenuItem;
061    private final JCheckBoxMenuItem nodeNumRequestMenuItem;
062    private final JRadioButtonMenuItem backgroundDisabled;
063    private final JRadioButtonMenuItem backgroundSlow;
064    private final JRadioButtonMenuItem backgroundFast;
065    private final JCheckBoxMenuItem addCommandStationMenuItem;
066    private final JCheckBoxMenuItem addNodesMenuItem;
067    private final JCheckBoxMenuItem startupCommandStationMenuItem;
068    private final JCheckBoxMenuItem startupNodesMenuItem;
069    private final JCheckBoxMenuItem startupNodesXmlMenuItem;
070    private final JRadioButtonMenuItem zeroBackups;
071    private final JRadioButtonMenuItem fiveBackups;
072    private final JRadioButtonMenuItem tenBackups;
073    private final JRadioButtonMenuItem twentyBackups;
074    private CbusDccProgrammerManager progMan;
075
076    /**
077     * {@inheritDoc}
078     */
079    @Override
080    public void initComponents(CanSystemConnectionMemo memo) {
081        super.initComponents(memo);
082
083        CbusConfigPaneProvider.loadInstances();
084
085        _selectedNode = -1;
086
087        preferences = memo.get(jmri.jmrix.can.cbus.CbusPreferences.class);
088        try {
089            progMan = memo.get(CbusConfigurationManager.class).get(GlobalProgrammerManager.class);
090        } catch (NullPointerException e) {
091            log.info("No Global Programmer available for NV programming");
092        }
093        init();
094
095    }
096
097    /**
098     * Create a new NodeConfigToolPane
099     */
100    public NodeConfigToolPane() {
101        super();
102        nodeNumRequestMenuItem = new JCheckBoxMenuItem(("Listen for Node Number Requests"));
103        teachNodeFromFcuFile = new JMenuItem(("Restore Node / Import Data from FCU XML")); //  FCU
104        searchForNodesMenuItem = new JMenuItem("Search for Nodes and Command Stations");
105        addCommandStationMenuItem = new JCheckBoxMenuItem(("Add Command Stations when found"));
106        addNodesMenuItem = new JCheckBoxMenuItem(("Add Nodes when found"));
107        backgroundDisabled = new JRadioButtonMenuItem(Bundle.getMessage("HighlightDisabled"));
108        backgroundSlow = new JRadioButtonMenuItem(("Slow"));
109        backgroundFast = new JRadioButtonMenuItem(("Fast"));
110
111        startupCommandStationMenuItem = new JCheckBoxMenuItem(("Search Command Stations on Startup"));
112        startupNodesMenuItem = new JCheckBoxMenuItem(("Search Nodes on Startup"));
113
114        startupNodesXmlMenuItem = new JCheckBoxMenuItem(("Add previously seen Nodes on Startup"));
115        zeroBackups = new JRadioButtonMenuItem(("0"));
116        fiveBackups = new JRadioButtonMenuItem(("5"));
117        tenBackups = new JRadioButtonMenuItem(("10"));
118        twentyBackups = new JRadioButtonMenuItem(("20"));
119    }
120
121    protected final ArrayList<CbusNodeConfigTab> getTabs() {
122        if (tabbedPanes==null) {
123            tabbedPanes = new ArrayList<>(6);
124            tabbedPanes.add( new CbusNodeInfoPane(this));
125            tabbedPanes.add( new CbusNodeUserCommentsPane(this));
126            tabbedPanes.add( new CbusNodeEditNVarPane(this));
127            tabbedPanes.add( new CbusNodeEventVarPane(this));
128            tabbedPanes.add( new CbusNodeSetupPane(this));
129            tabbedPanes.add( new CbusNodeBackupsPane(this));
130        }
131        return new ArrayList<>(this.tabbedPanes);
132    }
133
134    /**
135     * Initialise the NodeConfigToolPane
136     */
137    public void init() {
138
139        setMenuOptions(); // called when memo available
140
141        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
142
143        // main pane
144        JPanel _pane1 = new JPanel();
145        _pane1.setLayout(new BorderLayout());
146
147        // basis for future menu-bar if one required
148
149       // buttoncontainer.setLayout(new BorderLayout());
150       // updatenodesButton = new JButton(("Search for Nodes"));
151       // buttoncontainer.add(updatenodesButton);
152       // JPanel toppanelcontainer = new JPanel();
153       // toppanelcontainer.setLayout(new BoxLayout(toppanelcontainer, BoxLayout.X_AXIS));
154       // toppanelcontainer.add(buttoncontainer);
155       // pane1.add(toppanelcontainer, BorderLayout.PAGE_START);
156
157        // scroller for main table
158        nodeTablePane = new CbusNodeTablePane();
159        nodeTablePane.initComponents(memo);
160
161        nodeTable = nodeTablePane.nodeTable;
162
163        eventScroll = new JScrollPane( nodeTablePane );
164
165        JPanel mainNodePane = new JPanel();
166
167        mainNodePane.setLayout(new BorderLayout());
168        mainNodePane.add(eventScroll);
169
170        tabbedPane = new JTabbedPane();
171
172        tabbedPane.setEnabled(false);
173
174        getTabs().forEach((pn) -> {
175            tabbedPane.addTab(pn.getTitle(),pn);
176        });
177
178        Dimension minimumSize = new Dimension(40, 40);
179        mainNodePane.setMinimumSize(minimumSize);
180        tabbedPane.setMinimumSize(minimumSize);
181
182        this.setPreferredSize(new Dimension(700, 450));
183
184        split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, mainNodePane, tabbedPane);
185        split.setDividerLocation(preferences.getNodeTableSplit()); // px from top of node table pane
186        split.setContinuousLayout(true);
187        _pane1.add(split, BorderLayout.CENTER);
188        split.addPropertyChangeListener((PropertyChangeEvent changeEvent) -> {
189            JSplitPane sourceSplitPane = (JSplitPane) changeEvent.getSource();
190            String propertyName = changeEvent.getPropertyName();
191            if (propertyName.equals(JSplitPane.DIVIDER_LOCATION_PROPERTY)) {
192                preferences.setNodeTableSplit(sourceSplitPane.getDividerLocation());
193            }
194        });
195
196        add(_pane1);
197        _pane1.setVisible(true);
198
199        tabbedPane.addChangeListener((ChangeEvent e) -> {
200            userViewChanged();
201        });
202
203        // also add listener to tab action
204        nodeTable.getSelectionModel().addListSelectionListener((ListSelectionEvent e) -> {
205            if ( !e.getValueIsAdjusting() ) {
206                userViewChanged();
207            }
208        });
209
210        userViewChanged();
211
212        tabbedPane.setTransferHandler(new TransferHandler());
213        nodeTable.setTransferHandler(new TransferHandler());
214
215        revalidate();
216
217    }
218
219    private final JFrame topFrame = (JFrame) javax.swing.SwingUtilities.getWindowAncestor(this);
220
221    /**
222     * Create a document-modal Dialog with node search results.
223     * @param csfound number of Command Stations
224     * @param ndfound number of nodes
225     */
226    public void notifyNodeSearchComplete(int csfound, int ndfound){
227        busy_dialog.finish();
228        busy_dialog=null;
229
230        JmriJOptionPane.showMessageDialog(this, "<html><h3>Node Responses : " + ndfound +
231            "</h3><p>Of which Command Stations: " + csfound + "</p></html>", "Node Search Complete", JmriJOptionPane.INFORMATION_MESSAGE);
232        searchForNodesMenuItem.setEnabled(true);
233    }
234
235    /**
236     * Notify this pane that the selected node or viewed tab has changed
237     */
238    protected void userViewChanged(){
239
240        int sel = nodeTable.getSelectedRow();
241        int rowBefore = nodeTable.getSelectedRow()-1;
242        int rowAfter = nodeTable.getSelectedRow()+1;
243        if ( sel > -1 ) {
244            tabbedPane.setEnabled(true);
245
246
247            _selectedNode = (int) nodeTable.getModel().getValueAt(nodeTable.convertRowIndexToModel(sel), CbusNodeTableDataModel.NODE_NUMBER_COLUMN);
248
249            int tabindex = tabbedPane.getSelectedIndex();
250
251            int nodeBefore = -1;
252            int nodeAfter = -1;
253
254            if ( rowBefore > -1 ) {
255                nodeBefore = (int) nodeTable.getModel().getValueAt(rowBefore, CbusNodeTableDataModel.NODE_NUMBER_COLUMN);
256            }
257            if ( rowAfter < nodeTable.getRowCount() ) {
258                nodeAfter = (int) nodeTable.getModel().getValueAt(rowAfter, CbusNodeTableDataModel.NODE_NUMBER_COLUMN);
259            }
260
261            log.debug("node {} selected tab index {} , node before {} node after {}", _selectedNode , tabindex, nodeBefore,nodeAfter );
262
263            boolean veto = false;
264            for (CbusNodeConfigTab tab : getTabs()) {
265                if ( tab.getActiveDialog() || tab.getVetoBeingChanged()) {
266                    veto = true;
267                    break; // or return obj
268                }
269            }
270
271            if (veto){
272                return;
273            }
274
275            tabbedPane.setSelectedIndex(tabindex);
276            nodeTable.setRowSelectionInterval(sel,sel);
277
278            // this also starts urgent fetch loop if not currently looping
279            getNodeModel().setUrgentFetch(_selectedNode,nodeBefore,nodeAfter);
280
281            getTabs().get(tabindex).setNode( getNodeModel().getNodeByNodeNum(_selectedNode) );
282
283            try {
284                ((CbusDccProgrammer)(progMan.getGlobalProgrammer())).setNodeOfInterest(getNodeModel().getNodeByNodeNum(_selectedNode));
285            } catch(NullPointerException e) {
286                log.info("No programmer available fro NV programming");
287            }
288        }
289        else {
290            tabbedPane.setEnabled(false);
291
292        }
293    }
294
295    /**
296     * Set Menu Options eg. which checkboxes etc. should be checked
297     */
298    private void setMenuOptions(){
299
300        nodeNumRequestMenuItem.setSelected(
301                preferences.getAllocateNNListener() );
302        backgroundDisabled.setSelected(false);
303        backgroundSlow.setSelected(false);
304        backgroundFast.setSelected(false);
305
306        switch ((int) preferences.getNodeBackgroundFetchDelay()) {
307            case 0:
308                backgroundDisabled.setSelected(true);
309                break;
310            case 50:
311                backgroundFast.setSelected(true);
312                break;
313            case 100:
314                backgroundSlow.setSelected(true);
315                break;
316            default:
317                break;
318        }
319
320        addCommandStationMenuItem.setSelected( preferences.getAddCommandStations() );
321        addNodesMenuItem.setSelected( preferences.getAddNodes() );
322
323        startupCommandStationMenuItem.setSelected( preferences.getStartupSearchForCs() );
324        startupNodesMenuItem.setSelected( preferences.getStartupSearchForNodes() );
325        startupNodesXmlMenuItem.setSelected( preferences.getSearchForNodesBackupXmlOnStartup() );
326
327        zeroBackups.setSelected(false);
328        fiveBackups.setSelected(false);
329        tenBackups.setSelected(false);
330        twentyBackups.setSelected(false);
331
332        switch (preferences.getMinimumNumBackupsToKeep()) {
333            case 0:
334                zeroBackups.setSelected(true);
335                break;
336            case 5:
337                fiveBackups.setSelected(true);
338                break;
339            case 10:
340                tenBackups.setSelected(true);
341                break;
342            case 20:
343                twentyBackups.setSelected(true);
344                break;
345            default:
346                break;
347        }
348
349    }
350
351    /**
352     * Creates a Menu List.
353     * {@inheritDoc}
354     */
355    @Override
356    public List<JMenu> getMenus() {
357        List<JMenu> menuList = new ArrayList<>();
358
359        JMenu fileMenu = new JMenu(Bundle.getMessage("MenuFile"));
360
361        fileMenu.add(teachNodeFromFcuFile);
362
363        JMenu optionsMenu = new JMenu("Options");
364
365
366
367        JMenuItem sendSysResetMenuItem = new JMenuItem("Send System Reset");
368
369        searchForNodesMenuItem.setToolTipText(("Timeout set to " + NODE_SEARCH_TIMEOUT + "ms."));
370
371
372        nodeNumRequestMenuItem.setToolTipText("Also adds a check for any node already awaiting a number when performing node searches.");
373
374
375
376        JMenu backgroundFetchMenu = new JMenu("Node Info Fetch Speed");
377        ButtonGroup backgroundFetchGroup = new ButtonGroup();
378
379
380
381        JMenu numBackupsMenu = new JMenu("Min. Auto Backups to retain");
382        ButtonGroup minNumBackupsGroup = new ButtonGroup();
383
384
385
386        minNumBackupsGroup.add(zeroBackups);
387        minNumBackupsGroup.add(fiveBackups);
388        minNumBackupsGroup.add(tenBackups);
389        minNumBackupsGroup.add(twentyBackups);
390
391        numBackupsMenu.add(zeroBackups);
392        numBackupsMenu.add(fiveBackups);
393        numBackupsMenu.add(tenBackups);
394        numBackupsMenu.add(twentyBackups);
395
396        backgroundFetchGroup.add(backgroundDisabled);
397        backgroundFetchGroup.add(backgroundSlow);
398        backgroundFetchGroup.add(backgroundFast);
399
400        backgroundFetchMenu.add(backgroundDisabled);
401        backgroundFetchMenu.add(backgroundSlow);
402        backgroundFetchMenu.add(backgroundFast);
403
404        optionsMenu.add( searchForNodesMenuItem );
405        optionsMenu.add( new JSeparator() );
406        optionsMenu.add( sendSysResetMenuItem );
407        optionsMenu.add( new JSeparator() );
408        optionsMenu.add( backgroundFetchMenu );
409        optionsMenu.add( new JSeparator() );
410        optionsMenu.add( nodeNumRequestMenuItem );
411        optionsMenu.add( new JSeparator() );
412        optionsMenu.add( addCommandStationMenuItem );
413        optionsMenu.add( addNodesMenuItem );
414        optionsMenu.add( new JSeparator() );
415        optionsMenu.add( startupCommandStationMenuItem );
416        optionsMenu.add( startupNodesMenuItem );
417        optionsMenu.add( startupNodesXmlMenuItem );
418        optionsMenu.add( new JSeparator() );
419        optionsMenu.add( numBackupsMenu );
420
421        menuList.add(fileMenu);
422        menuList.add(optionsMenu);
423
424        ActionListener teachNodeFcu = ae -> {
425            fcuFrame = new CbusNodeRestoreFcuFrame(this);
426            fcuFrame.initComponents(memo);
427        };
428
429        teachNodeFromFcuFile.addActionListener(teachNodeFcu);
430
431        // saved preferences go through the cbus table model so they can be actioned immediately
432        // they'll be also saved by the table, not here.
433
434        ActionListener updatenodes = ae -> {
435            searchForNodesMenuItem.setEnabled(false);
436            busy_dialog = new jmri.util.swing.BusyDialog(topFrame, "Node Search", false);
437            busy_dialog.start();
438            getNodeModel().startASearchForNodes( this , NODE_SEARCH_TIMEOUT );
439        };
440        searchForNodesMenuItem.addActionListener(updatenodes);
441
442        ActionListener systemReset = ae -> {
443            new CbusSend(memo).aRST();
444            // flash something to user so they know that something has happened
445            busy_dialog = new jmri.util.swing.BusyDialog(topFrame, "System Reset", false);
446            busy_dialog.start();
447            ThreadingUtil.runOnGUIDelayed( () -> {
448                busy_dialog.finish();
449                busy_dialog=null;
450            },300 );
451        };
452        sendSysResetMenuItem.addActionListener(systemReset);
453
454        ActionListener nodeRequestActive = ae -> {
455            getNodeModel().setBackgroundAllocateListener( nodeNumRequestMenuItem.isSelected() );
456            preferences.setAllocateNNListener( nodeNumRequestMenuItem.isSelected() );
457        };
458        nodeNumRequestMenuItem.addActionListener(nodeRequestActive);
459
460        // values need to match setMenuOptions()
461        ActionListener fetchListener = ae -> {
462            if ( backgroundDisabled.isSelected() ) {
463                preferences.setNodeBackgroundFetchDelay(0L);
464                getNodeModel().startBackgroundFetch();
465            }
466            else if ( backgroundSlow.isSelected() ) {
467                preferences.setNodeBackgroundFetchDelay(100L);
468                getNodeModel().startBackgroundFetch();
469            }
470            else if ( backgroundFast.isSelected() ) {
471                preferences.setNodeBackgroundFetchDelay(50L);
472                getNodeModel().startBackgroundFetch();
473            }
474        };
475        backgroundDisabled.addActionListener(fetchListener);
476        backgroundSlow.addActionListener(fetchListener);
477        backgroundFast.addActionListener(fetchListener);
478
479        ActionListener addCsListener = ae -> {
480            preferences.setAddCommandStations( addCommandStationMenuItem.isSelected() );
481        };
482        addCommandStationMenuItem.addActionListener(addCsListener);
483
484        ActionListener addNodeListener = ae -> {
485            preferences.setAddNodes( addNodesMenuItem.isSelected() );
486        };
487        addNodesMenuItem.addActionListener(addNodeListener);
488
489        ActionListener addstartupCommandStationMenuItem = ae -> {
490            preferences.setStartupSearchForCs( startupCommandStationMenuItem.isSelected() );
491        };
492        startupCommandStationMenuItem.addActionListener(addstartupCommandStationMenuItem);
493
494        ActionListener addstartupNodesMenuItem = ae -> {
495            preferences.setStartupSearchForNodes( startupNodesMenuItem.isSelected() );
496        };
497        startupNodesMenuItem.addActionListener(addstartupNodesMenuItem);
498
499        ActionListener addstartupNodesXmlMenuItem = ae -> {
500            preferences.setSearchForNodesBackupXmlOnStartup( startupNodesXmlMenuItem.isSelected() );
501        };
502        startupNodesXmlMenuItem.addActionListener(addstartupNodesXmlMenuItem);
503
504         // values need to match setMenuOptions()
505        ActionListener minBackupsListener = ae -> {
506            if ( zeroBackups.isSelected() ) {
507                preferences.setMinimumNumBackupsToKeep(0);
508            }
509            else if ( fiveBackups.isSelected() ) {
510                preferences.setMinimumNumBackupsToKeep(5);
511            }
512            else if ( tenBackups.isSelected() ) {
513                preferences.setMinimumNumBackupsToKeep(10);
514            }
515            else if ( twentyBackups.isSelected() ) {
516                preferences.setMinimumNumBackupsToKeep(10);
517            }
518        };
519        zeroBackups.addActionListener(minBackupsListener);
520        fiveBackups.addActionListener(minBackupsListener);
521        tenBackups.addActionListener(minBackupsListener);
522        twentyBackups.addActionListener(minBackupsListener);
523
524
525        return menuList;
526    }
527
528    /**
529     * Set Restore from FCU Menu Item active as only 1 instance per NodeConfigToolPane allowed
530     * @param isActive set true if Frame opened, else false to notify closed
531     */
532    protected void setRestoreFcuActive( boolean isActive ){
533        teachNodeFromFcuFile.setEnabled(!isActive);
534    }
535
536    /**
537     * {@inheritDoc}
538     */
539    @Override
540    public String getTitle() {
541        return prependConnToString(Bundle.getMessage("MenuItemNodeConfig"));
542    }
543
544    /**
545     * {@inheritDoc}
546     */
547    @Override
548    public String getHelpTarget() {
549        return "package.jmri.jmrix.can.cbus.swing.nodeconfig.NodeConfigToolPane";
550    }
551
552    /**
553     * {@inheritDoc}
554     */
555    @Override
556    public void dispose() {
557
558        if (_toNode!=null){
559            _toNode.removePropertyChangeListener(this);
560        }
561
562      //  nodeTable = null;
563      //  eventScroll = null;
564
565        // May need to take a node out of learn mode so signal that we are closing
566        // Currently only applies to servo modules and the NV edit gui pane
567        getTabs().forEach((pn) -> {
568            if (pn instanceof CbusNodeEditNVarPane) {
569                pn.dispose();
570            }
571        });
572
573        super.dispose();
574    }
575
576    /**
577     * Handles drag actions containing CBUS events to edit / teach to a node
578     */
579    private class TransferHandler extends javax.swing.TransferHandler {
580        /**
581         * {@inheritDoc}
582         */
583        @Override
584        public boolean canImport(JComponent c, DataFlavor[] transferFlavors) {
585
586            // prevent draggable on startup when a node has not yet been selected
587            // the draggable must still be able to select a table row
588            if ( (c instanceof JTabbedPane ) && ( _selectedNode < 1 )){
589                return false;
590            }
591
592            for (DataFlavor flavor : transferFlavors) {
593                if (DataFlavor.stringFlavor.equals(flavor)) {
594                    return true;
595                }
596            }
597            return false;
598        }
599
600        /**
601         * {@inheritDoc}
602         */
603        @Override
604        public boolean importData(JComponent c, Transferable t) {
605            if (canImport(c, t.getTransferDataFlavors())) {
606
607                String eventInJmriFormat;
608                try {
609                    eventInJmriFormat = (String) t.getTransferData(DataFlavor.stringFlavor);
610                } catch (UnsupportedFlavorException | IOException e) {
611                    log.error("unable to get dragged address", e);
612                    return false;
613                }
614                return openNewOrEditEventFrame(eventInJmriFormat);
615            }
616            return false;
617        }
618    }
619
620    /**
621     * Opens a new or edit event frame depending on if existing
622     * @param eventInJmriFormat standard CBUS Sensor / Turnout phrase, eg "+44", "-N123E456", "X0A0B"
623     * @return false if issue opening or editing
624     */
625    private boolean openNewOrEditEventFrame( String eventInJmriFormat ){
626
627        // do some validation on the input string
628        // processed in the same way as a sensor, turnout or light so less chance of breaking in future
629        // and can also accept the Hex "X1234;X654876" format
630        String validatedAddr;
631        try {
632            validatedAddr = CbusAddress.validateSysName( eventInJmriFormat );
633        } catch (IllegalArgumentException e) {
634            return false;
635        }
636
637        CbusNode _node = getNodeModel().getNodeByNodeNum( _selectedNode );
638        if (_node==null){
639            log.warn("No Node");
640            return false;
641        }
642
643        CanMessage m = ( new CbusAddress(validatedAddr) ).makeMessage(0x12);
644
645        CbusNodeEvent newev = new CbusNodeEvent( memo,
646            CbusMessage.getNodeNumber(m),
647            CbusMessage.getEvent(m),
648            _selectedNode,
649            -1,
650            _node.getNodeParamManager().getParameter(5)
651            );
652        java.util.Arrays.fill(newev.getEvVarArray(),0);
653
654        log.debug("dragged nodeevent {} ",newev);
655        ThreadingUtil.runOnGUI( () -> {
656            getEditEvFrame().initComponents(memo,newev);
657        });
658        return true;
659    }
660
661    /**
662     * Get the edit event frame
663     * this could be requested from
664     * CbusNodeEventDataModel button click to edit event,
665     * this class when it receives an event via drag n drop,
666     * creating new event from CbusNodeEventVarPane
667     * @return the Frame
668     */
669    public CbusNodeEditEventFrame getEditEvFrame(){
670        if (_editEventFrame == null ){
671            _editEventFrame = new CbusNodeEditEventFrame(this);
672        }
673        return _editEventFrame;
674    }
675
676    /**
677     * Receive notification from the frame that it has disposed
678     */
679    protected void clearEditEventFrame() {
680        _editEventFrame = null;
681    }
682
683    private boolean _clearEvents;
684    private boolean _teachEvents;
685    private CbusNode _fromNode;
686    private CbusNode _toNode;
687    private JFrame _frame;
688
689    /**
690     * Show a Confirm before Save Dialogue Box then start teach process for Node
691     * <p>
692     * Used in Node Backup restore, Restore from FCU, edit NV's
693     * Edit Event variables currently use a custom dialogue, not this
694     * @param fromNode Node to get data from
695     * @param toNode Node to send changes to
696     * @param teachNVs true to Teach NV's
697     * @param clearEvents true to clear events before teaching new ones
698     * @param teachEvents true to teach events
699     * @param frame the frame to which dialogue boxes can be attached to
700     */
701    protected void showConfirmThenSave( @Nonnull CbusNode fromNode, @Nonnull CbusNode toNode,
702        boolean teachNVs, boolean clearEvents, boolean teachEvents, @CheckForNull JFrame frame){
703
704        _clearEvents = clearEvents;
705        _teachEvents = teachEvents;
706        _fromNode = fromNode;
707        _toNode = toNode;
708
709        if ( frame == null ){
710            frame = topFrame;
711        }
712        _frame = frame;
713
714        StringBuilder buf = new StringBuilder();
715        buf.append("<html> ")
716        .append( ("Please Confirm Write ") )
717        .append( ("to <br>") )
718        .append ( _toNode.toString() )
719        .append("<hr>");
720
721        if ( teachNVs ){
722
723            // Bundle.getMessage("NVConfirmWrite",nodeName)
724            buf.append("Teaching ")
725            .append(_toNode.getNodeNvManager().getNvDifference(_fromNode))
726            .append(" of ").append(_fromNode.getNodeNvManager().getTotalNVs()).append(" NV's<br>");
727        }
728        if ( _clearEvents ){
729            buf.append("Clearing ").append(Math.max( 0,_toNode.getNodeEventManager().getTotalNodeEvents() )).append(" Events<br>");
730        }
731        if ( _teachEvents ){
732            buf.append("Teaching ").append(Math.max( 0,_fromNode.getNodeEventManager().getTotalNodeEvents() )).append(" Events<br>");
733        }
734        buf.append("</html>");
735
736        int response = JmriJOptionPane.showConfirmDialog(frame,
737                ( buf.toString() ),
738                ( ("Please Confirm Write to Node")),
739                JmriJOptionPane.OK_CANCEL_OPTION,
740                JmriJOptionPane.QUESTION_MESSAGE);
741        if ( response == JmriJOptionPane.OK_OPTION ) {
742            _toNode.addPropertyChangeListener(this);
743            busy_dialog = new jmri.util.swing.BusyDialog(frame, "Write NVs "+_fromNode.toString(), false);
744            busy_dialog.start();
745            // update main node name from fcu name
746            _toNode.setNameIfNoName( _fromNode.getUserName() );
747            // request the local nv model pass the nv update request to the CbusNode
748            if ( teachNVs ){
749                _toNode.getNodeNvManager().sendNvsToNode( _fromNode.getNodeNvManager().getNvArray());
750            }
751            else {
752                nVTeachComplete(0);
753            }
754        }
755    }
756
757    /** {@inheritDoc} */
758    @Override
759    public void propertyChange(PropertyChangeEvent ev){
760        if (ev.getPropertyName().equals("TEACHNVCOMPLETE")) {
761            jmri.util.ThreadingUtil.runOnGUIEventually( ()->{
762                nVTeachComplete((Integer) ev.getNewValue());
763            });
764        }
765        else if (ev.getPropertyName().equals("ADDALLEVCOMPLETE")) {
766            jmri.util.ThreadingUtil.runOnGUIEventually( ()->{
767                teachEventsComplete((Integer) ev.getNewValue());
768            });
769        }
770    }
771
772    /**
773     * Notification from CbusNode NV Teach is complete
774     * Starts check to see if clear events
775     * @param numErrors number of errors writing NVs
776     */
777    private void nVTeachComplete(int numErrors){
778        if ( numErrors > 0 ) {
779            JmriJOptionPane.showMessageDialog(_frame,
780                Bundle.getMessage("NVSetFailTitle",numErrors), Bundle.getMessage("WarningTitle"),
781                JmriJOptionPane.ERROR_MESSAGE);
782        }
783
784        if ( _clearEvents ){
785
786            busy_dialog.setTitle("Clear Events");
787
788            // node enter learn mode
789            _toNode.send.nodeEnterLearnEvMode( _toNode.getNodeNumber() );
790            // no response expected but we add a mini delay for other traffic
791
792            ThreadingUtil.runOnLayoutDelayed( () -> {
793                _toNode.send.nNCLR(_toNode.getNodeNumber());// no response expected
794            }, 150 );
795            ThreadingUtil.runOnLayoutDelayed( () -> {
796                // node exit learn mode
797                _toNode.send.nodeExitLearnEvMode( _toNode.getNodeNumber() ); // no response expected
798            }, jmri.jmrix.can.cbus.node.CbusNodeTimerManager.SINGLE_MESSAGE_TIMEOUT_TIME );
799            ThreadingUtil.runOnGUIDelayed( () -> {
800
801                clearEventsComplete();
802
803            }, ( jmri.jmrix.can.cbus.node.CbusNodeTimerManager.SINGLE_MESSAGE_TIMEOUT_TIME + 150 ) );
804        }
805        else {
806            clearEventsComplete();
807        }
808    }
809
810    /**
811     * When clear Events completed ( in nvTeachComplete )
812     * starts process for teaching events to Node
813     */
814    private void clearEventsComplete() {
815        ArrayList<CbusNodeEvent> arL = _fromNode.getNodeEventManager().getEventArray();
816        if ( _teachEvents){
817            if (arL==null){
818                log.error("No Event Array on Node {}",_fromNode);
819                teachEventsComplete(1);
820                return;
821            }
822            busy_dialog.setTitle("Teach Events");
823            _toNode.getNodeEventManager().sendNewEvSToNode( arL );
824        }
825        else {
826            teachEventsComplete(0);
827        }
828    }
829
830    /**
831     * Notification from CbusNode Event Teach is complete
832     * @param numErrors number of errors writing events
833     */
834    private void teachEventsComplete( int numErrors ) {
835        _toNode.removePropertyChangeListener(this);
836        busy_dialog.finish();
837        busy_dialog = null;
838        if (numErrors != 0 ) {
839            JmriJOptionPane.showMessageDialog(_frame,
840            Bundle.getMessage("NdEvVarWriteError"), Bundle.getMessage("WarningTitle"),
841            JmriJOptionPane.ERROR_MESSAGE);
842        }
843        _frame = null;
844        _toNode = null;
845    }
846
847    /**
848     * Get the System Connection Node Model
849     * @return System Connection Node Model
850     */
851    @Nonnull
852    protected CbusNodeTableDataModel getNodeModel(){
853        if ( memo == null ) {
854            throw new IllegalStateException("No System Connection Set, call initComponents(memo)");
855        }
856        return memo.get(CbusConfigurationManager.class)
857            .provide(CbusNodeTableDataModel.class);
858    }
859
860    /**
861     * Nested class to create one of these using old-style defaults.
862     * Used as a startup action
863     */
864    static public class Default extends jmri.jmrix.can.swing.CanNamedPaneAction {
865
866        public Default() {
867            super(Bundle.getMessage("MenuItemNodeConfig"),
868            new jmri.util.swing.sdi.JmriJFrameInterface(),
869            NodeConfigToolPane.class.getName(),
870            jmri.InstanceManager.getDefault(CanSystemConnectionMemo.class));
871        }
872    }
873
874    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(NodeConfigToolPane.class);
875
876}