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