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        // Dispose all tabs, each tab registers itself as a listener on its node,
568        // so any not disposed remain in memory reacting to node changes after closing.
569        // The NV edit pane may also need to take a node out of learn mode.
570        getTabs().forEach(CbusNodeConfigTab::dispose);
571
572        super.dispose();
573    }
574
575    /**
576     * Handles drag actions containing CBUS events to edit / teach to a node
577     */
578    private class TransferHandler extends javax.swing.TransferHandler {
579        /**
580         * {@inheritDoc}
581         */
582        @Override
583        public boolean canImport(JComponent c, DataFlavor[] transferFlavors) {
584
585            // prevent draggable on startup when a node has not yet been selected
586            // the draggable must still be able to select a table row
587            if ( (c instanceof JTabbedPane ) && ( _selectedNode < 1 )){
588                return false;
589            }
590
591            for (DataFlavor flavor : transferFlavors) {
592                if (DataFlavor.stringFlavor.equals(flavor)) {
593                    return true;
594                }
595            }
596            return false;
597        }
598
599        /**
600         * {@inheritDoc}
601         */
602        @Override
603        public boolean importData(JComponent c, Transferable t) {
604            if (canImport(c, t.getTransferDataFlavors())) {
605
606                String eventInJmriFormat;
607                try {
608                    eventInJmriFormat = (String) t.getTransferData(DataFlavor.stringFlavor);
609                } catch (UnsupportedFlavorException | IOException e) {
610                    log.error("unable to get dragged address", e);
611                    return false;
612                }
613                return openNewOrEditEventFrame(eventInJmriFormat);
614            }
615            return false;
616        }
617    }
618
619    /**
620     * Opens a new or edit event frame depending on if existing
621     * @param eventInJmriFormat standard CBUS Sensor / Turnout phrase, eg "+44", "-N123E456", "X0A0B"
622     * @return false if issue opening or editing
623     */
624    private boolean openNewOrEditEventFrame( String eventInJmriFormat ){
625
626        // do some validation on the input string
627        // processed in the same way as a sensor, turnout or light so less chance of breaking in future
628        // and can also accept the Hex "X1234;X654876" format
629        String validatedAddr;
630        try {
631            validatedAddr = CbusAddress.validateSysName( eventInJmriFormat );
632        } catch (IllegalArgumentException e) {
633            return false;
634        }
635
636        CbusNode _node = getNodeModel().getNodeByNodeNum( _selectedNode );
637        if (_node==null){
638            log.warn("No Node");
639            return false;
640        }
641
642        CanMessage m = ( new CbusAddress(validatedAddr) ).makeMessage(0x12);
643
644        CbusNodeEvent newev = new CbusNodeEvent( memo,
645            CbusMessage.getNodeNumber(m),
646            CbusMessage.getEvent(m),
647            _selectedNode,
648            -1,
649            _node.getNodeParamManager().getParameter(5)
650            );
651        java.util.Arrays.fill(newev.getEvVarArray(),0);
652
653        log.debug("dragged nodeevent {} ",newev);
654        ThreadingUtil.runOnGUI( () -> {
655            getEditEvFrame().initComponents(memo,newev);
656        });
657        return true;
658    }
659
660    /**
661     * Get the edit event frame
662     * this could be requested from
663     * CbusNodeEventDataModel button click to edit event,
664     * this class when it receives an event via drag n drop,
665     * creating new event from CbusNodeEventVarPane
666     * @return the Frame
667     */
668    public CbusNodeEditEventFrame getEditEvFrame(){
669        if (_editEventFrame == null ){
670            _editEventFrame = new CbusNodeEditEventFrame(this);
671        }
672        return _editEventFrame;
673    }
674
675    /**
676     * Receive notification from the frame that it has disposed
677     */
678    protected void clearEditEventFrame() {
679        _editEventFrame = null;
680    }
681
682    private boolean _clearEvents;
683    private boolean _teachEvents;
684    private CbusNode _fromNode;
685    private CbusNode _toNode;
686    private JFrame _frame;
687
688    /**
689     * Show a Confirm before Save Dialogue Box then start teach process for Node
690     * <p>
691     * Used in Node Backup restore, Restore from FCU, edit NV's
692     * Edit Event variables currently use a custom dialogue, not this
693     * @param fromNode Node to get data from
694     * @param toNode Node to send changes to
695     * @param teachNVs true to Teach NV's
696     * @param clearEvents true to clear events before teaching new ones
697     * @param teachEvents true to teach events
698     * @param frame the frame to which dialogue boxes can be attached to
699     */
700    protected void showConfirmThenSave( @Nonnull CbusNode fromNode, @Nonnull CbusNode toNode,
701        boolean teachNVs, boolean clearEvents, boolean teachEvents, @CheckForNull JFrame frame){
702
703        _clearEvents = clearEvents;
704        _teachEvents = teachEvents;
705        _fromNode = fromNode;
706        _toNode = toNode;
707
708        if ( frame == null ){
709            frame = topFrame;
710        }
711        _frame = frame;
712
713        StringBuilder buf = new StringBuilder();
714        buf.append("<html> ")
715        .append( ("Please Confirm Write ") )
716        .append( ("to <br>") )
717        .append ( _toNode.toString() )
718        .append("<hr>");
719
720        if ( teachNVs ){
721
722            // Bundle.getMessage("NVConfirmWrite",nodeName)
723            buf.append("Teaching ")
724            .append(_toNode.getNodeNvManager().getNvDifference(_fromNode))
725            .append(" of ").append(_fromNode.getNodeNvManager().getTotalNVs()).append(" NV's<br>");
726        }
727        if ( _clearEvents ){
728            buf.append("Clearing ").append(Math.max( 0,_toNode.getNodeEventManager().getTotalNodeEvents() )).append(" Events<br>");
729        }
730        if ( _teachEvents ){
731            buf.append("Teaching ").append(Math.max( 0,_fromNode.getNodeEventManager().getTotalNodeEvents() )).append(" Events<br>");
732        }
733        buf.append("</html>");
734
735        int response = JmriJOptionPane.showConfirmDialog(frame,
736                ( buf.toString() ),
737                ( ("Please Confirm Write to Node")),
738                JmriJOptionPane.OK_CANCEL_OPTION,
739                JmriJOptionPane.QUESTION_MESSAGE);
740        if ( response == JmriJOptionPane.OK_OPTION ) {
741            _toNode.addPropertyChangeListener(this);
742            busy_dialog = new jmri.util.swing.BusyDialog(frame, "Write NVs "+_fromNode.toString(), false);
743            busy_dialog.start();
744            // update main node name from fcu name
745            _toNode.setNameIfNoName( _fromNode.getUserName() );
746            // request the local nv model pass the nv update request to the CbusNode
747            if ( teachNVs ){
748                _toNode.getNodeNvManager().sendNvsToNode( _fromNode.getNodeNvManager().getNvArray());
749            }
750            else {
751                nVTeachComplete(0);
752            }
753        }
754    }
755
756    /** {@inheritDoc} */
757    @Override
758    public void propertyChange(PropertyChangeEvent ev){
759        if (ev.getPropertyName().equals("TEACHNVCOMPLETE")) {
760            jmri.util.ThreadingUtil.runOnGUIEventually( ()->{
761                nVTeachComplete((Integer) ev.getNewValue());
762            });
763        }
764        else if (ev.getPropertyName().equals("ADDALLEVCOMPLETE")) {
765            jmri.util.ThreadingUtil.runOnGUIEventually( ()->{
766                teachEventsComplete((Integer) ev.getNewValue());
767            });
768        }
769    }
770
771    /**
772     * Notification from CbusNode NV Teach is complete
773     * Starts check to see if clear events
774     * @param numErrors number of errors writing NVs
775     */
776    private void nVTeachComplete(int numErrors){
777        if ( numErrors > 0 ) {
778            JmriJOptionPane.showMessageDialog(_frame,
779                Bundle.getMessage("NVSetFailTitle",numErrors), Bundle.getMessage("WarningTitle"),
780                JmriJOptionPane.ERROR_MESSAGE);
781        }
782
783        if ( _clearEvents ){
784
785            busy_dialog.setTitle("Clear Events");
786
787            // node enter learn mode
788            _toNode.send.nodeEnterLearnEvMode( _toNode.getNodeNumber() );
789            // no response expected but we add a mini delay for other traffic
790
791            ThreadingUtil.runOnLayoutDelayed( () -> {
792                _toNode.send.nNCLR(_toNode.getNodeNumber());// no response expected
793            }, 150 );
794            ThreadingUtil.runOnLayoutDelayed( () -> {
795                // node exit learn mode
796                _toNode.send.nodeExitLearnEvMode( _toNode.getNodeNumber() ); // no response expected
797            }, jmri.jmrix.can.cbus.node.CbusNodeTimerManager.SINGLE_MESSAGE_TIMEOUT_TIME );
798            ThreadingUtil.runOnGUIDelayed( () -> {
799
800                clearEventsComplete();
801
802            }, ( jmri.jmrix.can.cbus.node.CbusNodeTimerManager.SINGLE_MESSAGE_TIMEOUT_TIME + 150 ) );
803        }
804        else {
805            clearEventsComplete();
806        }
807    }
808
809    /**
810     * When clear Events completed ( in nvTeachComplete )
811     * starts process for teaching events to Node
812     */
813    private void clearEventsComplete() {
814        ArrayList<CbusNodeEvent> arL = _fromNode.getNodeEventManager().getEventArray();
815        if ( _teachEvents){
816            if (arL==null){
817                log.error("No Event Array on Node {}",_fromNode);
818                teachEventsComplete(1);
819                return;
820            }
821            busy_dialog.setTitle("Teach Events");
822            _toNode.getNodeEventManager().sendNewEvSToNode( arL );
823        }
824        else {
825            teachEventsComplete(0);
826        }
827    }
828
829    /**
830     * Notification from CbusNode Event Teach is complete
831     * @param numErrors number of errors writing events
832     */
833    private void teachEventsComplete( int numErrors ) {
834        _toNode.removePropertyChangeListener(this);
835        busy_dialog.finish();
836        busy_dialog = null;
837        if (numErrors != 0 ) {
838            JmriJOptionPane.showMessageDialog(_frame,
839            Bundle.getMessage("NdEvVarWriteError"), Bundle.getMessage("WarningTitle"),
840            JmriJOptionPane.ERROR_MESSAGE);
841        }
842        _frame = null;
843        _toNode = null;
844    }
845
846    /**
847     * Get the System Connection Node Model
848     * @return System Connection Node Model
849     */
850    @Nonnull
851    protected CbusNodeTableDataModel getNodeModel(){
852        if ( memo == null ) {
853            throw new IllegalStateException("No System Connection Set, call initComponents(memo)");
854        }
855        return memo.get(CbusConfigurationManager.class)
856            .provide(CbusNodeTableDataModel.class);
857    }
858
859    /**
860     * Nested class to create one of these using old-style defaults.
861     * Used as a startup action
862     */
863    public static class Default extends jmri.jmrix.can.swing.CanNamedPaneAction {
864
865        public Default() {
866            super(Bundle.getMessage("MenuItemNodeConfig"),
867            new jmri.util.swing.sdi.JmriJFrameInterface(),
868            NodeConfigToolPane.class.getName(),
869            jmri.InstanceManager.getDefault(CanSystemConnectionMemo.class));
870        }
871    }
872
873    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(NodeConfigToolPane.class);
874
875}