001package jmri.jmrit.symbolicprog.tabbedframe;
002
003import java.awt.*;
004import java.awt.event.ActionEvent;
005import java.awt.event.ItemEvent;
006import java.awt.event.ItemListener;
007import java.util.ArrayList;
008import java.util.List;
009import javax.annotation.Nonnull;
010import javax.annotation.OverridingMethodsMustInvokeSuper;
011import javax.swing.*;
012
013import jmri.AddressedProgrammerManager;
014import jmri.GlobalProgrammerManager;
015import jmri.InstanceManager;
016import jmri.InvokeOnAnyThread;
017import jmri.InvokeOnGuiThread;
018import jmri.Programmer;
019import jmri.ProgrammingMode;
020import jmri.ShutDownTask;
021import jmri.UserPreferencesManager;
022import jmri.implementation.swing.SwingShutDownTask;
023import jmri.jmrit.XmlFile;
024import jmri.jmrit.decoderdefn.DecoderFile;
025import jmri.jmrit.decoderdefn.DecoderIndexFile;
026import jmri.jmrit.roster.*;
027import jmri.jmrit.symbolicprog.*;
028import jmri.util.BusyGlassPane;
029import jmri.util.FileUtil;
030import jmri.util.JmriJFrame;
031import jmri.util.ThreadingUtil;
032import jmri.util.swing.JmriJOptionPane;
033
034import org.jdom2.Attribute;
035import org.jdom2.Element;
036
037/**
038 * Frame providing a command station programmer from decoder definition files.
039 *
040 * @author Bob Jacobsen Copyright (C) 2001, 2004, 2005, 2008, 2014, 2018, 2019, 2025
041 * @author D Miller Copyright 2003, 2005
042 * @author Howard G. Penny Copyright (C) 2005
043 */
044abstract public class PaneProgFrame extends JmriJFrame
045        implements java.beans.PropertyChangeListener, PaneContainer {
046
047    // members to contain working variable, CV values
048    JLabel progStatus = new JLabel(Bundle.getMessage("StateIdle"));
049    CvTableModel cvModel;
050    VariableTableModel variableModel;
051
052    ResetTableModel resetModel;
053    JMenu resetMenu = null;
054
055    ArrayList<ExtraMenuTableModel> extraMenuModelList;
056    ArrayList<JMenu> extraMenuList = new ArrayList<>();
057
058    Programmer mProgrammer;
059    boolean noDecoder = false;
060
061    JMenuBar menuBar = new JMenuBar();
062
063    JPanel tempPane; // passed around during construction
064
065    boolean _opsMode;
066
067    boolean maxFnNumDirty = false;
068    String maxFnNumOld = "";
069    String maxFnNumNew = "";
070
071    RosterEntry _rosterEntry;
072    RosterEntryPane _rPane = null;
073    RosterPhysicsPane _physicsPane = null;
074    FunctionLabelPane _flPane = null;
075    RosterMediaPane _rMPane = null;
076    String _frameEntryId;
077
078    List<JPanel> paneList = new ArrayList<>();
079    int paneListIndex;
080
081    List<Element> decoderPaneList;
082
083    BusyGlassPane glassPane;
084    List<JComponent> activeComponents = new ArrayList<>();
085
086    String filename;
087    String programmerShowEmptyPanes = "";
088    String decoderShowEmptyPanes = "";
089    String decoderAllowResetDefaults = "";
090    String suppressFunctionLabels = "";
091    String suppressRosterMedia = "";
092
093    // GUI member declarations
094    JTabbedPane tabPane;
095    JToggleButton readChangesButton = new JToggleButton(Bundle.getMessage("ButtonReadChangesAllSheets"));
096    JToggleButton writeChangesButton = new JToggleButton(Bundle.getMessage("ButtonWriteChangesAllSheets"));
097    JToggleButton readAllButton = new JToggleButton(Bundle.getMessage("ButtonReadAllSheets"));
098    JToggleButton writeAllButton = new JToggleButton(Bundle.getMessage("ButtonWriteAllSheets"));
099
100    ItemListener l1;
101    ItemListener l2;
102    ItemListener l3;
103    ItemListener l4;
104
105    ShutDownTask decoderDirtyTask;
106    ShutDownTask fileDirtyTask;
107
108    // holds a count of incomplete threads launched at ctor time; goes to zero when they're done
109    public java.util.concurrent.atomic.AtomicInteger threadCount = new java.util.concurrent.atomic.AtomicInteger(0);
110    
111    public RosterEntryPane getRosterPane() { return _rPane;}
112    public FunctionLabelPane getFnLabelPane() { return _flPane;}
113
114    /**
115     * Abstract method to provide a JPanel setting the programming mode, if
116     * appropriate.
117     * <p>
118     * A null value is ignored (?)
119     * @return new mode panel for inclusion in the GUI
120     */
121    abstract protected JPanel getModePane();
122
123    @InvokeOnGuiThread
124    protected void installComponents() {
125
126        tabPane = new jmri.util.org.mitre.jawb.swing.DetachableTabbedPane(" : "+_frameEntryId);
127        
128        // create ShutDownTasks
129        if (decoderDirtyTask == null) {
130            decoderDirtyTask = new SwingShutDownTask("DecoderPro Decoder Window Check",
131                    Bundle.getMessage("PromptQuitWindowNotWrittenDecoder"), null, this) {
132                @Override
133                public boolean checkPromptNeeded() {
134                    return !checkDirtyDecoder();
135                }
136            };
137        }
138        jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).register(decoderDirtyTask);
139        if (fileDirtyTask == null) {
140            fileDirtyTask = new SwingShutDownTask("DecoderPro Decoder Window Check",
141                    Bundle.getMessage("PromptQuitWindowNotWrittenConfig"),
142                    Bundle.getMessage("PromptSaveQuit"), this) {
143                @Override
144                public boolean checkPromptNeeded() {
145                    return !checkDirtyFile();
146                }
147
148                @Override
149                public boolean doPrompt() {
150                    // storeFile returns false if failed, so abort shutdown
151                    return storeFile();
152                }
153            };
154        }
155        jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).register(fileDirtyTask);
156
157        // Create a menu bar
158        setJMenuBar(menuBar);
159
160        // add a "File" menu
161        JMenu fileMenu = new JMenu(Bundle.getMessage("MenuFile"));
162        menuBar.add(fileMenu);
163
164        // add a "Factory Reset" menu
165        resetMenu = new JMenu(Bundle.getMessage("MenuReset"));
166        menuBar.add(resetMenu);
167        resetMenu.add(new FactoryResetAction(Bundle.getMessage("MenuFactoryReset"), resetModel, this));
168        resetMenu.setEnabled(false);
169
170        // Add a save item
171        JMenuItem menuItem = new JMenuItem(Bundle.getMessage("MenuSaveNoDots"));
172        menuItem.addActionListener(e -> storeFile()
173
174        );
175        menuItem.setAccelerator(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_S, java.awt.event.KeyEvent.META_DOWN_MASK));
176        fileMenu.add(menuItem);
177
178        JMenu printSubMenu = new JMenu(Bundle.getMessage("MenuPrint"));
179        printSubMenu.add(new PrintAction(Bundle.getMessage("MenuPrintAll"), this, false));
180        printSubMenu.add(new PrintCvAction(Bundle.getMessage("MenuPrintCVs"), cvModel, this, false, _rosterEntry));
181        fileMenu.add(printSubMenu);
182
183        JMenu printPreviewSubMenu = new JMenu(Bundle.getMessage("MenuPrintPreview"));
184        printPreviewSubMenu.add(new PrintAction(Bundle.getMessage("MenuPrintPreviewAll"), this, true));
185        printPreviewSubMenu.add(new PrintCvAction(Bundle.getMessage("MenuPrintPreviewCVs"), cvModel, this, true, _rosterEntry));
186        fileMenu.add(printPreviewSubMenu);
187
188        // add "Import" submenu; this is hierarchical because
189        // some of the names are so long, and we expect more formats
190        JMenu importSubMenu = new JMenu(Bundle.getMessage("MenuImport"));
191        fileMenu.add(importSubMenu);
192        importSubMenu.add(new CsvImportAction(Bundle.getMessage("MenuImportCSV"), cvModel, this, progStatus));
193        importSubMenu.add(new Pr1ImportAction(Bundle.getMessage("MenuImportPr1"), cvModel, this, progStatus));
194        importSubMenu.add(new LokProgImportAction(Bundle.getMessage("MenuImportLokProg"), cvModel, this, progStatus));
195        importSubMenu.add(new QuantumCvMgrImportAction(Bundle.getMessage("MenuImportQuantumCvMgr"), cvModel, this, progStatus));
196        importSubMenu.add(new TcsImportAction(Bundle.getMessage("MenuImportTcsFile"), cvModel, variableModel, this, progStatus, _rosterEntry));
197        if (TcsDownloadAction.willBeEnabled()) {
198            importSubMenu.add(new TcsDownloadAction(Bundle.getMessage("MenuImportTcsCS"), cvModel, variableModel, this, progStatus, _rosterEntry));
199        }
200
201        // add "Export" submenu; this is hierarchical because
202        // some of the names are so long, and we expect more formats
203        JMenu exportSubMenu = new JMenu(Bundle.getMessage("MenuExport"));
204        fileMenu.add(exportSubMenu);
205        exportSubMenu.add(new CsvExportAction(Bundle.getMessage("MenuExportCSV"), cvModel, this));
206        exportSubMenu.add(new CsvExportModifiedAction(Bundle.getMessage("MenuExportCSVModified"), cvModel, this));
207        exportSubMenu.add(new Pr1ExportAction(Bundle.getMessage("MenuExportPr1DOS"), cvModel, this));
208        exportSubMenu.add(new Pr1WinExportAction(Bundle.getMessage("MenuExportPr1WIN"), cvModel, this));
209        exportSubMenu.add(new CsvExportVariablesAction(Bundle.getMessage("MenuExportVariables"), variableModel, this));
210        exportSubMenu.add(new TcsExportAction(Bundle.getMessage("MenuExportTcsFile"), cvModel, variableModel, _rosterEntry, this));
211        if (TcsDownloadAction.willBeEnabled()) {
212            exportSubMenu.add(new TcsUploadAction(Bundle.getMessage("MenuExportTcsCS"), cvModel, variableModel, _rosterEntry, this));
213        }
214
215        // Speed table submenu in File menu
216        ThreadingUtil.runOnGUIEventually( ()->{
217            JMenu speedTableSubMenu = new JMenu(Bundle.getMessage("MenuSpeedTable"));
218            fileMenu.add(speedTableSubMenu);
219            ButtonGroup SpeedTableNumbersGroup = new ButtonGroup();
220            UserPreferencesManager upm = InstanceManager.getDefault(UserPreferencesManager.class);
221            Object speedTableNumbersSelectionObj = upm.getProperty(SpeedTableNumbers.class.getName(), "selection");
222    
223            SpeedTableNumbers speedTableNumbersSelection =
224                    speedTableNumbersSelectionObj != null
225                    ? SpeedTableNumbers.valueOf(speedTableNumbersSelectionObj.toString())
226                    : null;
227    
228            for (SpeedTableNumbers speedTableNumbers : SpeedTableNumbers.values()) {
229                JRadioButtonMenuItem rbMenuItem = new JRadioButtonMenuItem(speedTableNumbers.toString());
230                rbMenuItem.addActionListener((ActionEvent event) -> {
231                    rbMenuItem.setSelected(true);
232                    upm.setProperty(SpeedTableNumbers.class.getName(), "selection", speedTableNumbers.name());
233                    JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("MenuSpeedTable_CloseReopenWindow"));
234                });
235                rbMenuItem.setSelected(speedTableNumbers == speedTableNumbersSelection);
236                speedTableSubMenu.add(rbMenuItem);
237                SpeedTableNumbersGroup.add(rbMenuItem);
238            }
239        });
240        
241        // to control size, we need to insert a single
242        // JPanel, then have it laid out with BoxLayout
243        JPanel pane = new JPanel();
244        tempPane = pane;
245
246        // general GUI config
247        pane.setLayout(new BorderLayout());
248
249        // most of the GUI is done from XML in readConfig() function
250        // which configures the tabPane
251        pane.add(tabPane, BorderLayout.CENTER);
252
253        // and put that pane into the JFrame
254        getContentPane().add(pane);
255
256        // configure GUI buttons
257        ThreadingUtil.runOnGUIEventually( ()->{
258            configureButtons();
259        });
260        
261    }
262
263    @InvokeOnGuiThread
264    void configureButtons() {
265        // set read buttons enabled state, tooltips
266        enableReadButtons();
267
268        readChangesButton.addItemListener(l1 = e -> {
269            if (e.getStateChange() == ItemEvent.SELECTED) {
270                prepGlassPane(readChangesButton);
271                readChangesButton.setText(Bundle.getMessage("ButtonStopReadChangesAll"));
272                readChanges();
273            } else {
274                if (_programmingPane != null) {
275                    _programmingPane.stopProgramming();
276                }
277                paneListIndex = paneList.size();
278                readChangesButton.setText(Bundle.getMessage("ButtonReadChangesAllSheets"));
279            }
280        });
281
282        readAllButton.addItemListener(l3 = e -> {
283            if (e.getStateChange() == ItemEvent.SELECTED) {
284                prepGlassPane(readAllButton);
285                readAllButton.setText(Bundle.getMessage("ButtonStopReadAll"));
286                readAll();
287            } else {
288                if (_programmingPane != null) {
289                    _programmingPane.stopProgramming();
290                }
291                paneListIndex = paneList.size();
292                readAllButton.setText(Bundle.getMessage("ButtonReadAllSheets"));
293            }
294        });
295
296        writeChangesButton.setToolTipText(Bundle.getMessage("TipWriteHighlightedValues"));
297        writeChangesButton.addItemListener(l2 = e -> {
298            if (e.getStateChange() == ItemEvent.SELECTED) {
299                prepGlassPane(writeChangesButton);
300                writeChangesButton.setText(Bundle.getMessage("ButtonStopWriteChangesAll"));
301                writeChanges();
302            } else {
303                if (_programmingPane != null) {
304                    _programmingPane.stopProgramming();
305                }
306                paneListIndex = paneList.size();
307                writeChangesButton.setText(Bundle.getMessage("ButtonWriteChangesAllSheets"));
308            }
309        });
310
311        writeAllButton.setToolTipText(Bundle.getMessage("TipWriteAllValues"));
312        writeAllButton.addItemListener(l4 = e -> {
313            if (e.getStateChange() == ItemEvent.SELECTED) {
314                prepGlassPane(writeAllButton);
315                writeAllButton.setText(Bundle.getMessage("ButtonStopWriteAll"));
316                writeAll();
317            } else {
318                if (_programmingPane != null) {
319                    _programmingPane.stopProgramming();
320                }
321                paneListIndex = paneList.size();
322                writeAllButton.setText(Bundle.getMessage("ButtonWriteAllSheets"));
323            }
324        });
325    }
326    
327    void setProgrammingGui(JPanel bottom) {
328        // see if programming mode is available
329        JPanel tempModePane = null;
330        if (!noDecoder) {
331            tempModePane = getModePane();
332        }
333        if (tempModePane != null) {
334            // if so, configure programming part of GUI
335            // add buttons
336            JPanel bottomButtons = new JPanel();
337            bottomButtons.setLayout(new BoxLayout(bottomButtons, BoxLayout.X_AXIS));
338
339            bottomButtons.add(readChangesButton);
340            bottomButtons.add(writeChangesButton);
341            bottomButtons.add(readAllButton);
342            bottomButtons.add(writeAllButton);
343            bottom.add(bottomButtons);
344
345            // add programming mode
346            bottom.add(new JSeparator(javax.swing.SwingConstants.HORIZONTAL));
347            JPanel temp = new JPanel();
348            bottom.add(temp);
349            temp.add(tempModePane);
350        } else {
351            // set title to Editing
352            super.setTitle(Bundle.getMessage("TitleEditPane", _frameEntryId));
353        }
354
355        // add space for (programming) status message
356        bottom.add(new JSeparator(javax.swing.SwingConstants.HORIZONTAL));
357        progStatus.setAlignmentX(JLabel.CENTER_ALIGNMENT);
358        bottom.add(progStatus);
359    }
360
361    // ================== Search section ==================
362
363    // create and add the Search GUI
364    void setSearchGui(JPanel bottom) {
365        // search field
366        searchBar = new jmri.util.swing.SearchBar(searchForwardTask, searchBackwardTask, searchDoneTask);
367        searchBar.setVisible(false); // start not visible
368        searchBar.configureKeyModifiers(this);
369        bottom.add(searchBar);
370    }
371
372    jmri.util.swing.SearchBar searchBar;
373    static class SearchPair {
374        WatchingLabel label;
375        JPanel tab;
376        SearchPair(WatchingLabel label, @Nonnull JPanel tab) {
377            this.label = label;
378            this.tab = tab;
379        }
380    }
381
382    ArrayList<SearchPair> searchTargetList;
383    int nextSearchTarget = 0;
384
385    // Load the array of search targets
386    protected void loadSearchTargets() {
387        if (searchTargetList != null) return;
388
389        searchTargetList = new ArrayList<>();
390
391        for (JPanel p : getPaneList()) {
392            for (Component c : p.getComponents()) {
393                loadJPanel(c, p);
394            }
395        }
396
397        // add the panes themselves
398        for (JPanel tab : getPaneList()) {
399            searchTargetList.add( new SearchPair( null, tab ));
400        }
401    }
402
403    // Recursive load of possible search targets
404    protected void loadJPanel(Component c, JPanel tab) {
405        if (c instanceof JPanel) {
406            for (Component d : ((JPanel)c).getComponents()) {
407                loadJPanel(d, tab);
408            }
409        } else if (c instanceof JScrollPane) {
410            loadJPanel( ((JScrollPane)c).getViewport().getView(), tab);
411        } else if (c instanceof WatchingLabel) {
412            searchTargetList.add( new SearchPair( (WatchingLabel)c, tab));
413        }
414    }
415
416    // Search didn't find anything at all
417    protected void searchDidNotFind() {
418         java.awt.Toolkit.getDefaultToolkit().beep();
419    }
420
421    // Search succeeded, go to the result
422    protected void searchGoesTo(SearchPair result) {
423        tabPane.setSelectedComponent(result.tab);
424        if (result.label != null) {
425            SwingUtilities.invokeLater(() -> result.label.getWatched().requestFocus());
426        } else {
427            log.trace("search result set to tab {}", result.tab);
428        }
429    }
430
431    // Check a single case to see if its search match
432    // @return true for matched
433    private boolean checkSearchTarget(int index, String target) {
434        boolean result = false;
435        if (searchTargetList.get(index).label != null ) {
436            // match label text
437            if ( ! searchTargetList.get(index).label.getText().toUpperCase().contains(target.toUpperCase() ) ) {
438                return false;
439            }
440            // only match if showing
441            return searchTargetList.get(index).label.isShowing();
442        } else {
443            // Match pane label.
444            // Finding the tab requires a search here.
445            // Could have passed a clue along in SwingUtilities
446            for (int i = 0; i < tabPane.getTabCount(); i++) {
447                if (tabPane.getComponentAt(i) == searchTargetList.get(index).tab) {
448                    result = tabPane.getTitleAt(i).toUpperCase().contains(target.toUpperCase());
449                }
450            }
451        }
452        return result;
453    }
454
455    // Invoked by forward search operation
456    private final Runnable searchForwardTask = new Runnable() {
457        @Override
458        public void run() {
459            log.trace("start forward");
460            loadSearchTargets();
461            String target = searchBar.getSearchString();
462
463            nextSearchTarget++;
464            if (nextSearchTarget < 0 ) nextSearchTarget = 0;
465            if (nextSearchTarget >= searchTargetList.size() ) nextSearchTarget = 0;
466
467            int startingSearchTarget = nextSearchTarget;
468
469            while (nextSearchTarget < searchTargetList.size()) {
470                if ( checkSearchTarget(nextSearchTarget, target)) {
471                    // hit!
472                    searchGoesTo(searchTargetList.get(nextSearchTarget));
473                    return;
474                }
475                nextSearchTarget++;
476            }
477
478            // end reached, wrap
479            nextSearchTarget = 0;
480            while (nextSearchTarget < startingSearchTarget) {
481                if ( checkSearchTarget(nextSearchTarget, target)) {
482                    // hit!
483                    searchGoesTo(searchTargetList.get(nextSearchTarget));
484                    return;
485                }
486                nextSearchTarget++;
487            }
488            // not found
489            searchDidNotFind();
490        }
491    };
492
493    // Invoked by backward search operation
494    private final Runnable searchBackwardTask = new Runnable() {
495        @Override
496        public void run() {
497            log.trace("start backward");
498            loadSearchTargets();
499            String target = searchBar.getSearchString();
500
501            nextSearchTarget--;
502            if (nextSearchTarget < 0 ) nextSearchTarget = searchTargetList.size()-1;
503            if (nextSearchTarget >= searchTargetList.size() ) nextSearchTarget = searchTargetList.size()-1;
504
505            int startingSearchTarget = nextSearchTarget;
506
507            while (nextSearchTarget > 0) {
508                if ( checkSearchTarget(nextSearchTarget, target)) {
509                    // hit!
510                    searchGoesTo(searchTargetList.get(nextSearchTarget));
511                    return;
512                }
513                nextSearchTarget--;
514            }
515
516            // start reached, wrap
517            nextSearchTarget = searchTargetList.size() - 1;
518            while (nextSearchTarget > startingSearchTarget) {
519                if ( checkSearchTarget(nextSearchTarget, target)) {
520                    // hit!
521                    searchGoesTo(searchTargetList.get(nextSearchTarget));
522                    return;
523                }
524                nextSearchTarget--;
525            }
526            // not found
527            searchDidNotFind();
528        }
529    };
530
531    // Invoked when search bar Done is pressed
532    private final Runnable searchDoneTask = new Runnable() {
533        @Override
534        public void run() {
535            log.debug("done with search bar");
536            searchBar.setVisible(false);
537        }
538    };
539
540    // =================== End of search section ==================
541
542    public List<JPanel> getPaneList() {
543        return paneList;
544    }
545
546    void addHelp() {
547        addHelpMenu("package.jmri.jmrit.symbolicprog.tabbedframe.PaneProgFrame", true);
548    }
549
550    @Override
551    public Dimension getPreferredSize() {
552        Dimension screen = getMaximumSize();
553        int width = Math.min(super.getPreferredSize().width, screen.width);
554        int height = Math.min(super.getPreferredSize().height, screen.height);
555        return new Dimension(width, height);
556    }
557
558    @Override
559    public Dimension getMaximumSize() {
560        Dimension screen = getToolkit().getScreenSize();
561        return new Dimension(screen.width, screen.height - 35);
562    }
563
564    /**
565     * Enable the [Read all] and [Read changes] buttons if possible. This checks
566     * to make sure this is appropriate, given the attached programmer's
567     * capability.
568     */
569    void enableReadButtons() {
570        readChangesButton.setToolTipText(Bundle.getMessage("TipReadChanges"));
571        readAllButton.setToolTipText(Bundle.getMessage("TipReadAll"));
572        // check with CVTable programmer to see if read is possible
573        if (cvModel != null && cvModel.getProgrammer() != null
574                && !cvModel.getProgrammer().getCanRead()
575                || noDecoder) {
576            // can't read, disable the button
577            readChangesButton.setEnabled(false);
578            readAllButton.setEnabled(false);
579            readChangesButton.setToolTipText(Bundle.getMessage("TipNoRead"));
580            readAllButton.setToolTipText(Bundle.getMessage("TipNoRead"));
581        } else {
582            readChangesButton.setEnabled(true);
583            readAllButton.setEnabled(true);
584        }
585    }
586
587    /**
588     * Initialization sequence:
589     * <ul>
590     * <li> Ask the RosterEntry to read its contents
591     * <li> If the decoder file is specified, open and load it, otherwise get
592     * the decoder filename from the RosterEntry and load that. Note that we're
593     * assuming the roster entry has the right decoder, at least w.r.t. the loco
594     * file.
595     * <li> Fill CV values from the roster entry
596     * <li> Create the programmer panes
597     * </ul>
598     *
599     * @param pDecoderFile    XML file defining the decoder contents; if null,
600     *                        the decoder definition is found from the
601     *                        RosterEntry
602     * @param pRosterEntry    RosterEntry for information on this locomotive
603     * @param pFrameEntryId   Roster ID (entry) loaded into the frame
604     * @param pProgrammerFile Name of the programmer file to use
605     * @param pProg           Programmer object to be used to access CVs
606     * @param opsMode         true for opsMode, else false.
607     */
608    public PaneProgFrame(DecoderFile pDecoderFile, @Nonnull RosterEntry pRosterEntry,
609            String pFrameEntryId, String pProgrammerFile, Programmer pProg, boolean opsMode) {
610        super(Bundle.getMessage("TitleProgPane", pFrameEntryId));
611
612        _rosterEntry = pRosterEntry;
613        _opsMode = opsMode;
614        filename = pProgrammerFile;
615        mProgrammer = pProg;
616        _frameEntryId = pFrameEntryId;
617
618        // create the tables
619        cvModel = new CvTableModel(progStatus, mProgrammer);
620
621        variableModel = new VariableTableModel(progStatus, new String[] {"Name", "Value"},
622                cvModel);
623
624        resetModel = new ResetTableModel(progStatus, mProgrammer);
625        extraMenuModelList = new ArrayList<>();
626
627        // handle the roster entry
628        _rosterEntry.setOpen(true);
629
630        installComponents();
631
632        threadCount.incrementAndGet();
633        new javax.swing.SwingWorker<Object, Object>(){
634            @Override
635            public Object doInBackground() {
636                if (_rosterEntry.getFileName() != null) {
637                    // set the loco file name in the roster entry
638                    _rosterEntry.readFile();  // read, but don't yet process
639                }
640        
641                log.trace("starting to load decoderfile");
642                if (pDecoderFile != null) {
643                    loadDecoderFile(pDecoderFile, _rosterEntry);
644                } else {
645                    loadDecoderFromLoco(pRosterEntry);
646                }
647                log.trace("end loading decoder file");
648                return null;
649            }
650            @Override
651            protected void done() {
652                ctorPhase2();
653                threadCount.decrementAndGet();
654            }
655        }.execute();
656    }
657    
658    // This is invoked at the end of the 
659    // PaneProgFrame constructor, after the roster entry and DecoderFile
660    // have been read in
661    @InvokeOnGuiThread
662    void ctorPhase2() {
663        // save default values
664        saveDefaults();
665
666        // finally fill the Variable and CV values from the specific loco file
667        if (_rosterEntry.getFileName() != null) {
668            _rosterEntry.loadCvModel(variableModel, cvModel);
669        }
670
671        // mark file state as consistent
672        variableModel.setFileDirty(false);
673
674        // if the Reset Table was used lets enable the menu item
675        if (!_opsMode || resetModel.hasOpsModeReset()) {
676            if (resetModel.getRowCount() > 0) {
677                resetMenu.setEnabled(true);
678            }
679        }
680
681        // if there are extra menus defined, enable them
682        log.trace("enabling {} {}", extraMenuModelList.size(), extraMenuModelList);
683        for (int i = 0; i<extraMenuModelList.size(); i++) {
684            log.trace("enabling {} {}", _opsMode, extraMenuModelList.get(i).hasOpsModeReset());
685            if ( !_opsMode || extraMenuModelList.get(i).hasOpsModeReset()) {
686                if (extraMenuModelList.get(i).getRowCount() > 0) {
687                    extraMenuList.get(i).setEnabled(true);
688                }
689            }
690        }
691
692        // set the programming mode
693        if (mProgrammer != null) {
694            if (InstanceManager.getOptionalDefault(AddressedProgrammerManager.class).isPresent()
695                    || InstanceManager.getOptionalDefault(GlobalProgrammerManager.class).isPresent()) {
696                // go through in preference order, trying to find a mode
697                // that exists in both the programmer and decoder.
698                // First, get attributes. If not present, assume that
699                // all modes are usable
700                Element programming = null;
701                if (decoderRoot != null
702                        && (programming = decoderRoot.getChild("decoder").getChild("programming")) != null) {
703
704                    // add a verify-write facade if configured
705                    Programmer pf = mProgrammer;
706                    if (getDoConfirmRead()) {
707                        pf = new jmri.implementation.VerifyWriteProgrammerFacade(pf);
708                        log.debug("adding VerifyWriteProgrammerFacade, new programmer is {}", pf);
709                    }
710                    // add any facades defined in the decoder file
711                    pf = jmri.implementation.ProgrammerFacadeSelector
712                            .loadFacadeElements(programming, pf, getCanCacheDefault(), mProgrammer);
713                    log.debug("added any other FacadeElements, new programmer is {}", pf);
714                    mProgrammer = pf;
715                    cvModel.setProgrammer(pf);
716                    resetModel.setProgrammer(pf);
717                    for (var model : extraMenuModelList) {
718                        model.setProgrammer(pf);
719                    }
720                    log.debug("Found programmer: {}", cvModel.getProgrammer());
721                }
722
723                // done after setting facades in case new possibilities appear
724                if (programming != null) {
725                    pickProgrammerMode(programming);
726                    // reset the read buttons if the mode changes
727                    enableReadButtons();
728                    if (noDecoder) {
729                        writeChangesButton.setEnabled(false);
730                        writeAllButton.setEnabled(false);
731                    }
732                } else {
733                    log.debug("Skipping programmer setup because found no programmer element");
734                }
735
736            } else {
737                log.error("Can't set programming mode, no programmer instance");
738            }
739        }
740
741        // and build the GUI (after programmer mode because it depends on what's available)
742        loadProgrammerFile(_rosterEntry);
743
744        // optionally, add extra panes from the decoder file
745        Attribute a;
746        if ((a = programmerRoot.getChild("programmer").getAttribute("decoderFilePanes")) != null
747                && a.getValue().equals("yes")) {
748            if (decoderRoot != null) {
749                if (log.isDebugEnabled()) {
750                    log.debug("will process {} pane definitions from decoder file", decoderPaneList.size());
751                }
752                for (Element element : decoderPaneList) {
753                    // load each pane
754                    String pname = jmri.util.jdom.LocaleSelector.getAttribute(element, "name");
755
756                    // handle include/exclude
757                    if (isIncludedFE(element, modelElem, _rosterEntry, "", "")) {
758                        newPane(pname, element, modelElem, true, false);  // show even if empty not a programmer pane
759                        log.debug("PaneProgFrame init - pane {} added", pname); // these are MISSING in RosterPrint
760                    }
761                }
762            }
763        }
764
765        JPanel bottom = new JPanel();
766        bottom.setLayout(new BoxLayout(bottom, BoxLayout.Y_AXIS));
767        tempPane.add(bottom, BorderLayout.SOUTH);
768
769        // now that programmer is configured, set the programming GUI
770        setProgrammingGui(bottom);
771
772        // add the search GUI
773        setSearchGui(bottom);
774
775        pack();
776
777        if (log.isDebugEnabled()) {  // because size elements take time
778            log.debug("PaneProgFrame \"{}\" constructed for file {}, unconstrained size is {}, constrained to {}",
779                    _frameEntryId, _rosterEntry.getFileName(), super.getPreferredSize(), getPreferredSize());
780        }
781    }
782
783    /**
784     * Front end to DecoderFile.isIncluded()
785     * <ul>
786     * <li>Retrieves "productID" and "model attributes from the "model" element
787     * and "family" attribute from the roster entry. </li>
788     * <li>Then invokes DecoderFile.isIncluded() with the retrieved values.</li>
789     * <li>Deals gracefully with null or missing elements and
790     * attributes.</li>
791     * </ul>
792     *
793     * @param e             XML element with possible "include" and "exclude"
794     *                      attributes to be checked
795     * @param aModelElement "model" element from the Decoder Index, used to get
796     *                      "model" and "productID".
797     * @param aRosterEntry  The current roster entry, used to get "family".
798     * @param extraIncludes additional "include" terms
799     * @param extraExcludes additional "exclude" terms.
800     * @return true if front ended included, else false.
801     */
802    public static boolean isIncludedFE(Element e, Element aModelElement, RosterEntry aRosterEntry, String extraIncludes, String extraExcludes) {
803
804        String pID;
805        try {
806            pID = aModelElement.getAttribute("productID").getValue();
807        } catch (Exception ex) {
808            pID = null;
809        }
810
811        String modelName;
812        try {
813            modelName = aModelElement.getAttribute("model").getValue();
814        } catch (Exception ex) {
815            modelName = null;
816        }
817
818        String familyName;
819        try {
820            familyName = aRosterEntry.getDecoderFamily();
821        } catch (Exception ex) {
822            familyName = null;
823        }
824        return DecoderFile.isIncluded(e, pID, modelName, familyName, extraIncludes, extraExcludes);
825    }
826
827    protected void pickProgrammerMode(@Nonnull Element programming) {
828        log.debug("pickProgrammerMode starts");
829        boolean paged = true;
830        boolean directbit = true;
831        boolean directbyte = true;
832        boolean register = true;
833
834        Attribute a;
835
836        // set the programming attributes for DCC
837        if ((a = programming.getAttribute("nodecoder")) != null) {
838            if (a.getValue().equals("yes")) {
839                noDecoder = true;   // No decoder in the loco
840            }
841        }
842        if ((a = programming.getAttribute("paged")) != null) {
843            if (a.getValue().equals("no")) {
844                paged = false;
845            }
846        }
847        if ((a = programming.getAttribute("direct")) != null) {
848            if (a.getValue().equals("no")) {
849                directbit = false;
850                directbyte = false;
851            } else if (a.getValue().equals("bitOnly")) {
852                //directbit = true;
853                directbyte = false;
854            } else if (a.getValue().equals("byteOnly")) {
855                directbit = false;
856                //directbyte = true;
857            //} else { // items already have these values
858                //directbit = true;
859                //directbyte = true;
860            }
861        }
862        if ((a = programming.getAttribute("register")) != null) {
863            if (a.getValue().equals("no")) {
864                register = false;
865            }
866        }
867
868        // find an accepted mode to set it to
869        List<ProgrammingMode> modes = mProgrammer.getSupportedModes();
870
871        if (log.isDebugEnabled()) {
872            log.debug("XML specifies modes: P {} DBi {} Dby {} R {} now {}", paged, directbit, directbyte, register, mProgrammer.getMode());
873            log.debug("Programmer supports:");
874            for (ProgrammingMode m : modes) {
875                log.debug(" mode: {} {}", m.getStandardName(), m);
876            }
877        }
878
879        StringBuilder desiredModes = new StringBuilder();
880        // first try specified modes
881        for (Element el1 : programming.getChildren("mode")) {
882            String name = el1.getText();
883            if (desiredModes.length() > 0) desiredModes.append(", ");
884            desiredModes.append(name);
885            log.debug(" mode {} was specified", name);
886            for (ProgrammingMode m : modes) {
887                if (name.equals(m.getStandardName())) {
888                    log.debug("Programming mode selected: {} ({})", m, m.getStandardName());
889                    mProgrammer.setMode(m);
890                    return;
891                }
892            }
893        }
894
895        // go through historical modes
896        if (modes.contains(ProgrammingMode.DIRECTMODE) && directbit && directbyte) {
897            mProgrammer.setMode(ProgrammingMode.DIRECTMODE);
898            log.debug("Set to DIRECTMODE");
899        } else if (modes.contains(ProgrammingMode.DIRECTBITMODE) && directbit) {
900            mProgrammer.setMode(ProgrammingMode.DIRECTBITMODE);
901            log.debug("Set to DIRECTBITMODE");
902        } else if (modes.contains(ProgrammingMode.DIRECTBYTEMODE) && directbyte) {
903            mProgrammer.setMode(ProgrammingMode.DIRECTBYTEMODE);
904            log.debug("Set to DIRECTBYTEMODE");
905        } else if (modes.contains(ProgrammingMode.PAGEMODE) && paged) {
906            mProgrammer.setMode(ProgrammingMode.PAGEMODE);
907            log.debug("Set to PAGEMODE");
908        } else if (modes.contains(ProgrammingMode.REGISTERMODE) && register) {
909            mProgrammer.setMode(ProgrammingMode.REGISTERMODE);
910            log.debug("Set to REGISTERMODE");
911        } else if (noDecoder) {
912            log.debug("No decoder");
913        } else {
914            JmriJOptionPane.showMessageDialog(
915                    this,
916                    Bundle.getMessage("ErrorCannotSetMode", desiredModes.toString()),
917                    Bundle.getMessage("ErrorCannotSetModeTitle"),
918                    JmriJOptionPane.ERROR_MESSAGE);
919            log.warn("No acceptable mode found, leave as found");
920        }
921    }
922
923    /**
924     * Data element holding the 'model' element representing the decoder type.
925     */
926    Element modelElem = null;
927
928    Element decoderRoot = null;
929
930    protected void loadDecoderFromLoco(RosterEntry r) {
931        // get a DecoderFile from the locomotive xml
932        String decoderModel = r.getDecoderModel();
933        String decoderFamily = r.getDecoderFamily();
934        log.debug("selected loco uses decoder {} {}", decoderFamily, decoderModel);
935
936        // locate a decoder like that.
937        List<DecoderFile> l = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, decoderFamily, null, null, null, decoderModel);
938        log.debug("found {} matches", l.size());
939        if (l.size() == 0) {
940            log.debug("Loco uses {} {} decoder, but no such decoder defined", decoderFamily, decoderModel);
941            // fall back to use just the decoder name, not family
942            l = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, null, null, null, null, decoderModel);
943            if (log.isDebugEnabled()) {
944                log.debug("found {} matches without family key", l.size());
945            }
946        }
947        if (l.size() > 0) {
948            DecoderFile d = l.get(0);
949            loadDecoderFile(d, r);
950        } else {
951            if (decoderModel.equals("")) {
952                log.debug("blank decoderModel requested, so nothing loaded");
953            } else {
954                log.warn("no matching \"{}\" decoder found for loco, no decoder info loaded", decoderModel);
955            }
956        }
957    }
958
959    protected void loadDecoderFile(@Nonnull DecoderFile df, @Nonnull RosterEntry re) {
960        if (log.isDebugEnabled()) {
961            log.debug("loadDecoderFile from {} {}", DecoderFile.fileLocation, df.getFileName());
962        }
963
964        try {
965            decoderRoot = df.rootFromName(DecoderFile.fileLocation + df.getFileName());
966        } catch (org.jdom2.JDOMException e) {
967            log.error("Exception while parsing decoder XML file: {}", df.getFileName(), e);
968            return;
969        } catch (java.io.IOException e) {
970            log.error("Exception while reading decoder XML file: {}", df.getFileName(), e);
971            return;
972        }
973        // load variables from decoder tree
974        df.getProductID();
975        df.loadVariableModel(decoderRoot.getChild("decoder"), variableModel);
976
977        // load reset from decoder tree
978        df.loadResetModel(decoderRoot.getChild("decoder"), resetModel);
979
980        // load extra menus from decoder tree
981        df.loadExtraMenuModel(decoderRoot.getChild("decoder"), extraMenuModelList, progStatus, mProgrammer);
982
983        // add extra menus
984        log.trace("add menus {} {}", extraMenuModelList.size(), extraMenuList);
985        for (int i=0; i < extraMenuModelList.size(); i++ ) {
986            String name = extraMenuModelList.get(i).getName();
987            JMenu menu = new JMenu(name);
988            extraMenuList.add(i, menu);
989            menuBar.add(menu);
990            menu.add(new ExtraMenuAction(name, extraMenuModelList.get(i), this));
991            menu.setEnabled(false);
992        }
993
994        // add Window and Help menu items (_after_ the extra menus)
995        addHelp();
996
997        // load function names from family
998        re.loadFunctions(decoderRoot.getChild("decoder").getChild("family").getChild("functionlabels"), "family");
999
1000        // load sound names from family
1001        re.loadSounds(decoderRoot.getChild("decoder").getChild("family").getChild("soundlabels"), "family");
1002
1003        // get the showEmptyPanes attribute, if yes/no update our state
1004        if (decoderRoot.getAttribute("showEmptyPanes") != null) {
1005            log.debug("Found in decoder showEmptyPanes={}", decoderRoot.getAttribute("showEmptyPanes").getValue());
1006            decoderShowEmptyPanes = decoderRoot.getAttribute("showEmptyPanes").getValue();
1007        } else {
1008            decoderShowEmptyPanes = "";
1009        }
1010        log.debug("decoderShowEmptyPanes={}", decoderShowEmptyPanes);
1011
1012        // get the suppressFunctionLabels attribute, if yes/no update our state
1013        if (decoderRoot.getAttribute("suppressFunctionLabels") != null) {
1014            log.debug("Found in decoder suppressFunctionLabels={}", decoderRoot.getAttribute("suppressFunctionLabels").getValue());
1015            suppressFunctionLabels = decoderRoot.getAttribute("suppressFunctionLabels").getValue();
1016        } else {
1017            suppressFunctionLabels = "";
1018        }
1019        log.debug("suppressFunctionLabels={}", suppressFunctionLabels);
1020
1021        // get the suppressRosterMedia attribute, if yes/no update our state
1022        if (decoderRoot.getAttribute("suppressRosterMedia") != null) {
1023            log.debug("Found in decoder suppressRosterMedia={}", decoderRoot.getAttribute("suppressRosterMedia").getValue());
1024            suppressRosterMedia = decoderRoot.getAttribute("suppressRosterMedia").getValue();
1025        } else {
1026            suppressRosterMedia = "";
1027        }
1028        log.debug("suppressRosterMedia={}", suppressRosterMedia);
1029
1030        // get the allowResetDefaults attribute, if yes/no update our state
1031        if (decoderRoot.getAttribute("allowResetDefaults") != null) {
1032            log.debug("Found in decoder allowResetDefaults={}", decoderRoot.getAttribute("allowResetDefaults").getValue());
1033            decoderAllowResetDefaults = decoderRoot.getAttribute("allowResetDefaults").getValue();
1034        } else {
1035            decoderAllowResetDefaults = "yes";
1036        }
1037        log.debug("decoderAllowResetDefaults={}", decoderAllowResetDefaults);
1038
1039        // save the pointer to the model element
1040        modelElem = df.getModelElement();
1041
1042        // load function names from model
1043        re.loadFunctions(modelElem.getChild("functionlabels"), "model");
1044
1045        // load sound names from model
1046        re.loadSounds(modelElem.getChild("soundlabels"), "model");
1047
1048        // load maxFnNum from model
1049        Attribute a;
1050        if ((a = modelElem.getAttribute("maxFnNum")) != null) {
1051            maxFnNumOld = re.getMaxFnNum();
1052            maxFnNumNew = a.getValue();
1053            if (!maxFnNumOld.equals(maxFnNumNew)) {
1054                if (!re.getId().equals(Bundle.getMessage("LabelNewDecoder"))) {
1055                    maxFnNumDirty = true;
1056                    log.debug("maxFnNum for \"{}\" changed from {} to {}", re.getId(), maxFnNumOld, maxFnNumNew);
1057                    String message = java.text.MessageFormat.format(
1058                            SymbolicProgBundle.getMessage("StatusMaxFnNumUpdated"),
1059                            re.getDecoderFamily(), re.getDecoderModel(), maxFnNumNew);
1060                    progStatus.setText(message);
1061                }
1062                re.setMaxFnNum(maxFnNumNew);
1063            }
1064        }
1065    }
1066
1067    protected void loadProgrammerFile(RosterEntry r) {
1068        // Open and parse programmer file
1069        XmlFile pf = new XmlFile() {
1070        };  // XmlFile is abstract
1071        try {
1072            programmerRoot = pf.rootFromName(filename);
1073
1074            // get the showEmptyPanes attribute, if yes/no update our state
1075            if (programmerRoot.getChild("programmer").getAttribute("showEmptyPanes") != null) {
1076                programmerShowEmptyPanes = programmerRoot.getChild("programmer").getAttribute("showEmptyPanes").getValue();
1077                log.debug("Found in programmer {}", programmerShowEmptyPanes);
1078            } else {
1079                programmerShowEmptyPanes = "";
1080            }
1081
1082            // get extra any panes from the programmer file
1083            Attribute a;
1084            if ((a = programmerRoot.getChild("programmer").getAttribute("decoderFilePanes")) != null
1085                    && a.getValue().equals("yes")) {
1086                if (decoderRoot != null) {
1087                    decoderPaneList = decoderRoot.getChildren("pane");
1088                }
1089            }
1090
1091            // load programmer config from programmer tree
1092            readConfig(programmerRoot, r);
1093
1094        } catch (org.jdom2.JDOMException e) {
1095            log.error("exception parsing programmer file: {}", filename, e);
1096        } catch (java.io.IOException e) {
1097            log.error("exception reading programmer file: {}", filename, e);
1098        }
1099    }
1100
1101    Element programmerRoot = null;
1102
1103    /**
1104     * @return true if decoder needs to be written
1105     */
1106    protected boolean checkDirtyDecoder() {
1107        if (log.isDebugEnabled()) {
1108            log.debug("Checking decoder dirty status. CV: {} variables:{}", cvModel.decoderDirty(), variableModel.decoderDirty());
1109        }
1110        return (getModePane() != null && (cvModel.decoderDirty() || variableModel.decoderDirty()));
1111    }
1112
1113    /**
1114     * @return true if file needs to be written
1115     */
1116    protected boolean checkDirtyFile() {
1117        return (variableModel.fileDirty() ||
1118                _rPane.guiChanged(_rosterEntry) ||
1119                _flPane.guiChanged(_rosterEntry) ||
1120                _rMPane.guiChanged(_rosterEntry) ||
1121                (_physicsPane != null && _physicsPane.guiChanged(_rosterEntry)) ||
1122                maxFnNumDirty);
1123    }
1124
1125    protected void handleDirtyFile() {
1126    }
1127
1128    /**
1129     * Close box has been clicked; handle check for dirty with respect to
1130     * decoder or file, then close.
1131     *
1132     * @param e Not used
1133     */
1134    @Override
1135    public void windowClosing(java.awt.event.WindowEvent e) {
1136
1137        // Don't want to actually close if we return early
1138        setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
1139
1140        // check for various types of dirty - first table data not written back
1141        if (log.isDebugEnabled()) {
1142            log.debug("Checking decoder dirty status. CV: {} variables:{}", cvModel.decoderDirty(), variableModel.decoderDirty());
1143        }
1144        if (!noDecoder && checkDirtyDecoder()) {
1145            if (JmriJOptionPane.showConfirmDialog(this,
1146                    Bundle.getMessage("PromptCloseWindowNotWrittenDecoder"),
1147                    Bundle.getMessage("PromptChooseOne"),
1148                    JmriJOptionPane.OK_CANCEL_OPTION) != JmriJOptionPane.OK_OPTION) {
1149                return;
1150            }
1151        }
1152        if (checkDirtyFile()) {
1153            int option = JmriJOptionPane.showOptionDialog(this, Bundle.getMessage("PromptCloseWindowNotWrittenConfig"),
1154                    Bundle.getMessage("PromptChooseOne"),
1155                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.WARNING_MESSAGE, null,
1156                    new String[]{Bundle.getMessage("PromptSaveAndClose"), Bundle.getMessage("PromptClose"), Bundle.getMessage("ButtonCancel")},
1157                    Bundle.getMessage("PromptSaveAndClose"));
1158            if (option == 0) { // array position 0 PromptSaveAndClose
1159                // save requested
1160                if (!storeFile()) {
1161                    return;   // don't close if failed
1162                }
1163            } else if (option == 2 || option == JmriJOptionPane.CLOSED_OPTION ) {
1164                // cancel requested or Dialog closed
1165                return; // without doing anything
1166            }
1167        }
1168        if(maxFnNumDirty && !maxFnNumOld.equals("")){
1169            _rosterEntry.setMaxFnNum(maxFnNumOld);
1170        }
1171        // Check for a "<new loco>" roster entry; if found, remove it
1172        List<RosterEntry> l = Roster.getDefault().matchingList(null, null, null, null, null, null, Bundle.getMessage("LabelNewDecoder"));
1173        if (l.size() > 0 && log.isDebugEnabled()) {
1174            log.debug("Removing {} <new loco> entries", l.size());
1175        }
1176        int x = l.size() + 1;
1177        while (l.size() > 0) {
1178            Roster.getDefault().removeEntry(l.get(0));
1179            l = Roster.getDefault().matchingList(null, null, null, null, null, null, Bundle.getMessage("LabelNewDecoder"));
1180            x--;
1181            if (x == 0) {
1182                log.error("We have tried to remove all the entries, however an error has occurred which has resulted in the entries not being deleted correctly");
1183                l = new ArrayList<>();
1184            }
1185        }
1186
1187        // OK, continue close
1188        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
1189
1190        // deregister shutdown hooks
1191        jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).deregister(decoderDirtyTask);
1192        decoderDirtyTask = null;
1193        jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).deregister(fileDirtyTask);
1194        fileDirtyTask = null;
1195
1196        // do the close itself
1197        super.windowClosing(e);
1198    }
1199
1200    void readConfig(Element root, RosterEntry r) {
1201         // check for "programmer" element at start
1202        Element base;
1203        if ((base = root.getChild("programmer")) == null) {
1204            log.error("xml file top element is not programmer");
1205            return;
1206        }
1207
1208        // add the Info tab
1209        _rPane = new RosterEntryPane(r);
1210        _rPane.setMaximumSize(_rPane.getPreferredSize());
1211        if (root.getChild("programmer").getAttribute("showRosterPane") != null) {
1212            if (root.getChild("programmer").getAttribute("showRosterPane").getValue().equals("no")) {
1213                makeInfoPane(r);
1214            } else {
1215                final int i = tabPane.getTabCount();
1216                tabPane.addTab(Bundle.getMessage("ROSTER ENTRY"), makeStandinComponent());
1217                threadCount.incrementAndGet();
1218                new javax.swing.SwingWorker<JComponent, Object>(){
1219                    @Override
1220                    public JComponent doInBackground() {
1221                       return makeInfoPane(r);
1222                    }
1223                    @Override
1224                    protected void done() {
1225                        try {
1226                            var result = get();
1227                            tabPane.setComponentAt(i, result);
1228                        } catch (InterruptedException | java.util.concurrent.ExecutionException e) {
1229                            log.error("Exception",e);
1230                        }
1231                        threadCount.decrementAndGet();
1232                    }
1233                }.execute();
1234            }
1235        } else {
1236            final int i = tabPane.getTabCount();
1237            tabPane.addTab(Bundle.getMessage("ROSTER ENTRY"), makeStandinComponent());
1238            
1239            threadCount.incrementAndGet();
1240            new javax.swing.SwingWorker<JComponent, Object>(){
1241                @Override
1242                public JComponent doInBackground() {
1243                   return makeInfoPane(r);
1244                }
1245                @Override
1246                protected void done() {
1247                    try {
1248                        var result = get();
1249                        tabPane.setComponentAt(i, result);
1250                    } catch (InterruptedException | java.util.concurrent.ExecutionException e) {
1251                        log.error("Exception",e);
1252                    }
1253                    threadCount.decrementAndGet();
1254                }
1255            }.execute();
1256        }
1257
1258        // add the Physics tab (locomotive-level Physics parameters)
1259        // Follow the roster pane visibility setting
1260        if (root.getChild("programmer").getAttribute("showRosterPane") != null &&
1261                root.getChild("programmer").getAttribute("showRosterPane").getValue().equals("no")) {
1262            // create it, just don't make it visible
1263            _physicsPane = new RosterPhysicsPane(r);
1264            makePhysicsPane(r);
1265        } else {
1266            _physicsPane = new RosterPhysicsPane(r);
1267            final int iPhys = tabPane.getTabCount();
1268            tabPane.addTab(RosterPhysicsPane.getTabTitle(), makeStandinComponent());
1269            threadCount.incrementAndGet();
1270            new javax.swing.SwingWorker<JComponent, Object>() {
1271                @Override
1272                public JComponent doInBackground() {
1273                    return makePhysicsPane(r);
1274                }
1275
1276                @Override
1277                protected void done() {
1278                    try {
1279                        var result = get();
1280                        tabPane.setComponentAt(iPhys, result);
1281                    } catch (
1282                            InterruptedException |
1283                            java.util.concurrent.ExecutionException e) {
1284                        log.error("Exception", e);
1285                    }
1286                    threadCount.decrementAndGet();
1287                }
1288            }.execute();
1289        }
1290
1291        // add the Function Label tab
1292        if (root.getChild("programmer").getAttribute("showFnLanelPane").getValue().equals("yes")
1293                && !suppressFunctionLabels.equals("yes")
1294            ) {
1295                       
1296                final int i = tabPane.getTabCount();
1297                tabPane.addTab(Bundle.getMessage("FUNCTION LABELS"), makeStandinComponent());
1298                
1299                threadCount.incrementAndGet();
1300                new javax.swing.SwingWorker<JComponent, Object>(){
1301                    @Override
1302                    public JComponent doInBackground() {
1303                       return makeFunctionLabelPane(r);
1304                    }
1305                    @Override
1306                    protected void done() {
1307                        try {
1308                            var result = get();
1309                            tabPane.setComponentAt(i, result);
1310                        } catch (InterruptedException | java.util.concurrent.ExecutionException e) {
1311                            log.error("Exception",e);
1312                        }
1313                        threadCount.decrementAndGet();
1314                    }
1315                }.execute();
1316            
1317        } else {
1318            // make it, just don't make it visible
1319            makeFunctionLabelPane(r);
1320        }
1321
1322        // add the Media tab
1323        if (root.getChild("programmer").getAttribute("showRosterMediaPane").getValue().equals("yes")
1324                && !suppressRosterMedia.equals("yes")
1325            ) {
1326
1327                final int i = tabPane.getTabCount();
1328                tabPane.addTab(Bundle.getMessage("ROSTER MEDIA"), makeStandinComponent());
1329                
1330                threadCount.incrementAndGet();
1331                new javax.swing.SwingWorker<JComponent, Object>(){
1332                    @Override
1333                    public JComponent doInBackground() {
1334                       return makeMediaPane(r);
1335                    }
1336                    @Override
1337                    protected void done() {
1338                        try {
1339                            var result = get();
1340                            tabPane.setComponentAt(i, result);
1341                        } catch (InterruptedException | java.util.concurrent.ExecutionException e) {
1342                            log.error("Exception",e);
1343                        }
1344                        threadCount.decrementAndGet();
1345                    }
1346                }.execute();
1347
1348        } else {
1349            // create it, just don't make it visible
1350            makeMediaPane(r);
1351        }
1352
1353        // add the comment tab
1354        JPanel commentTab = new JPanel();
1355        var comment = new JTextArea(_rPane.getCommentDocument());
1356        JScrollPane commentScroller = new JScrollPane(comment, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
1357        commentTab.add(commentScroller);
1358        commentTab.setLayout(new BoxLayout(commentTab, BoxLayout.Y_AXIS));
1359        tabPane.addTab(Bundle.getMessage("COMMENT PANE"), commentTab);
1360
1361        // for all "pane" elements in the programmer
1362        List<Element> progPaneList = base.getChildren("pane");
1363        log.debug("will process {} pane definitions", progPaneList.size());
1364        
1365        for (Element temp : progPaneList) {
1366            // load each programmer pane
1367            List<Element> pnames = temp.getChildren("name");
1368            boolean isProgPane = true;
1369            if ((pnames.size() > 0) && (decoderPaneList != null) && (decoderPaneList.size() > 0)) {
1370                String namePrimary = (pnames.get(0)).getValue(); // get non-localised name
1371
1372                // check if there is a same-name pane in decoder file
1373                // start at end to prevent concurrentmodification exception on remove
1374                for (int j = decoderPaneList.size() - 1; j >= 0; j--) {
1375                    List<Element> dnames = decoderPaneList.get(j).getChildren("name");
1376                    if (dnames.size() > 0) {
1377                        String namePrimaryDecoder = (dnames.get(0)).getValue(); // get non-localised name
1378                        if (namePrimary.equals(namePrimaryDecoder)) {
1379                            // replace programmer pane with same-name decoder pane
1380                            temp = decoderPaneList.get(j);
1381                            decoderPaneList.remove(j); // safe, not suspicious as we work end - front
1382                            isProgPane = false;
1383                        }
1384                    }
1385                }
1386            }
1387            String name = jmri.util.jdom.LocaleSelector.getAttribute(temp, "name");
1388
1389            // handle include/exclude
1390            if (isIncludedFE(temp, modelElem, _rosterEntry, "", "")) {
1391                newPane(name, temp, modelElem, false, isProgPane);  // don't force showing if empty
1392                log.debug("readConfig - pane {} added", name); // these are also in RosterPrint
1393            }
1394        }
1395        log.trace("done processing panes");
1396    }
1397
1398    /**
1399     * Make temporary contents for a pane while loading
1400     */
1401    protected Component makeStandinComponent() {
1402        var retval = new JPanel(){
1403                            @Override
1404                            public Dimension getPreferredSize() {
1405                                // return a nominal size for the tabbed panes until manually resized
1406                                return new java.awt.Dimension(900, 600);
1407                            }
1408        };
1409        retval.add(new JLabel(Bundle.getMessage("STANDIN MESSAGE")));
1410        return retval;
1411    }
1412    
1413    
1414    /**
1415     * Reset all CV values to defaults stored earlier.
1416     * <p>
1417     * This will in turn update the variables.
1418     */
1419    protected void resetToDefaults() {
1420        int n = defaultCvValues.length;
1421        for (int i = 0; i < n; i++) {
1422            CvValue cv = cvModel.getCvByNumber(defaultCvNumbers[i]);
1423            if (cv == null) {
1424                log.warn("Trying to set default in CV {} but didn't find the CV object", defaultCvNumbers[i]);
1425            } else {
1426                cv.setValue(defaultCvValues[i]);
1427            }
1428        }
1429    }
1430
1431    int[] defaultCvValues = null;
1432    String[] defaultCvNumbers = null;
1433
1434    /**
1435     * Save all CV values.
1436     * <p>
1437     * These stored values are used by {link #resetToDefaults()}
1438     */
1439    protected void saveDefaults() {
1440        int n = cvModel.getRowCount();
1441        defaultCvValues = new int[n];
1442        defaultCvNumbers = new String[n];
1443
1444        for (int i = 0; i < n; i++) {
1445            CvValue cv = cvModel.getCvByRow(i);
1446            defaultCvValues[i] = cv.getValue();
1447            defaultCvNumbers[i] = cv.number();
1448        }
1449    }
1450
1451    @InvokeOnAnyThread  // transfers some operations to GUI thread
1452    protected JPanel makeInfoPane(RosterEntry r) {
1453        // create the identification pane (not configured by programmer file now; maybe later?)
1454
1455        JPanel outer = new JPanel();
1456        ThreadingUtil.runOnGUI(()->{
1457            outer.setLayout(new BoxLayout(outer, BoxLayout.Y_AXIS));
1458            JPanel body = new JPanel();
1459            body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS));
1460            JScrollPane scrollPane = new JScrollPane(body);
1461    
1462            // add roster info
1463            body.add(_rPane);
1464    
1465            // add the store button
1466            JButton store = new JButton(Bundle.getMessage("ButtonSave"));
1467            store.setAlignmentX(JLabel.CENTER_ALIGNMENT);
1468            store.addActionListener(e -> storeFile());
1469    
1470            // add the reset button
1471            JButton reset = new JButton(Bundle.getMessage("ButtonResetDefaults"));
1472            reset.setAlignmentX(JLabel.CENTER_ALIGNMENT);
1473            if (decoderAllowResetDefaults.equals("no")) {
1474                reset.setEnabled(false);
1475                reset.setToolTipText(Bundle.getMessage("TipButtonResetDefaultsDisabled"));
1476            } else {
1477                reset.setToolTipText(Bundle.getMessage("TipButtonResetDefaults"));
1478                reset.addActionListener(e -> resetToDefaults());
1479            }
1480    
1481            int sizeX = Math.max(reset.getPreferredSize().width, store.getPreferredSize().width);
1482            int sizeY = Math.max(reset.getPreferredSize().height, store.getPreferredSize().height);
1483            store.setPreferredSize(new Dimension(sizeX, sizeY));
1484            reset.setPreferredSize(new Dimension(sizeX, sizeY));
1485    
1486            store.setToolTipText(_rosterEntry.getFileName());
1487    
1488            JPanel buttons = new JPanel();
1489            buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
1490    
1491            buttons.add(store);
1492            buttons.add(reset);
1493    
1494            body.add(buttons);
1495            outer.add(scrollPane);
1496    
1497            // arrange for the dcc address to be updated
1498            java.beans.PropertyChangeListener dccNews = e -> updateDccAddress();
1499            primaryAddr = variableModel.findVar("Short Address");
1500            if (primaryAddr == null) {
1501                log.debug("DCC Address monitor didn't find a Short Address variable");
1502            } else {
1503                primaryAddr.addPropertyChangeListener(dccNews);
1504            }
1505            extendAddr = variableModel.findVar("Long Address");
1506            if (extendAddr == null) {
1507                log.debug("DCC Address monitor didn't find an Long Address variable");
1508            } else {
1509                extendAddr.addPropertyChangeListener(dccNews);
1510            }
1511            addMode = (EnumVariableValue) variableModel.findVar("Address Format");
1512            if (addMode == null) {
1513                log.debug("DCC Address monitor didn't find an Address Format variable");
1514            } else {
1515                addMode.addPropertyChangeListener(dccNews);
1516            }
1517    
1518            // get right address to start
1519            updateDccAddress();
1520        });
1521        
1522        return outer;
1523    }
1524
1525
1526    @InvokeOnAnyThread  // transfers some operations to GUI thread
1527    protected JPanel makePhysicsPane(RosterEntry r) {
1528        // create the physics pane wrapper (not configured by programmer file)
1529        JPanel outer = new JPanel();
1530        ThreadingUtil.runOnGUI(() -> {
1531            outer.setLayout(new BoxLayout(outer, BoxLayout.Y_AXIS));
1532            JPanel body = new JPanel();
1533            body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS));
1534            JScrollPane scrollPane = new JScrollPane(body);
1535
1536            // add physics info
1537            if (_physicsPane != null) {
1538                body.add(_physicsPane);
1539            }
1540
1541            // add the store button
1542            JButton store = new JButton(Bundle.getMessage("ButtonSave"));
1543            store.setAlignmentX(JLabel.CENTER_ALIGNMENT);
1544            store.addActionListener(e -> storeFile());
1545            store.setToolTipText(_rosterEntry.getFileName());
1546            JPanel buttons = new JPanel();
1547            buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
1548            buttons.add(store);
1549            body.add(buttons);
1550
1551            outer.add(scrollPane);
1552        });
1553        return outer;
1554    }
1555
1556    @InvokeOnAnyThread // transfers some operations to GUI thread
1557    protected JPanel makeFunctionLabelPane(RosterEntry r) {
1558        // create the identification pane (not configured by programmer file now; maybe later?)
1559
1560        JPanel outer = new JPanel();
1561
1562        ThreadingUtil.runOnGUI(()->{
1563            outer.setLayout(new BoxLayout(outer, BoxLayout.Y_AXIS));
1564            JPanel body = new JPanel();
1565            body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS));
1566            JScrollPane scrollPane = new JScrollPane(body);
1567    
1568            // add tab description
1569            JLabel title = new JLabel(Bundle.getMessage("UseThisTabCustomize"));
1570            title.setAlignmentX(JLabel.CENTER_ALIGNMENT);
1571            body.add(title);
1572            body.add(new JLabel(" ")); // some padding
1573    
1574            // add roster info
1575            _flPane = new FunctionLabelPane(r);
1576            //_flPane.setMaximumSize(_flPane.getPreferredSize());
1577            body.add(_flPane);
1578    
1579            // add the store button
1580            JButton store = new JButton(Bundle.getMessage("ButtonSave"));
1581            store.setAlignmentX(JLabel.CENTER_ALIGNMENT);
1582            store.addActionListener(e -> storeFile());
1583    
1584            store.setToolTipText(_rosterEntry.getFileName());
1585    
1586            JPanel buttons = new JPanel();
1587            buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
1588    
1589            buttons.add(store);
1590    
1591            body.add(buttons);
1592            outer.add(scrollPane);
1593        });
1594        return outer;
1595    }
1596
1597    @InvokeOnAnyThread  // transfers some operations to GUI thread
1598    protected JPanel makeMediaPane(RosterEntry r) {
1599        // create the identification pane (not configured by programmer file now; maybe later?)
1600        JPanel outer = new JPanel();
1601
1602        ThreadingUtil.runOnGUI(()->{
1603            outer.setLayout(new BoxLayout(outer, BoxLayout.Y_AXIS));
1604            JPanel body = new JPanel();
1605            body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS));
1606            JScrollPane scrollPane = new JScrollPane(body);
1607    
1608            // add tab description
1609            JLabel title = new JLabel(Bundle.getMessage("UseThisTabMedia"));
1610            title.setAlignmentX(JLabel.CENTER_ALIGNMENT);
1611            body.add(title);
1612            body.add(new JLabel(" ")); // some padding
1613    
1614            // add roster info
1615            _rMPane = new RosterMediaPane(r);
1616            _rMPane.setMaximumSize(_rMPane.getPreferredSize());
1617            body.add(_rMPane);
1618    
1619            // add the store button
1620            JButton store = new JButton(Bundle.getMessage("ButtonSave"));
1621            store.setAlignmentX(JLabel.CENTER_ALIGNMENT);
1622            store.addActionListener(e -> storeFile());
1623    
1624            JPanel buttons = new JPanel();
1625            buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
1626    
1627            buttons.add(store);
1628    
1629            body.add(buttons);
1630            outer.add(scrollPane);
1631        });
1632        return outer;
1633    }
1634
1635    // hold refs to variables to check dccAddress
1636    VariableValue primaryAddr = null;
1637    VariableValue extendAddr = null;
1638    EnumVariableValue addMode = null;
1639
1640    boolean longMode = false;
1641    String newAddr = null;
1642
1643    void updateDccAddress() {
1644
1645        if (log.isDebugEnabled()) {
1646            log.debug("updateDccAddress: short {} long {} mode {}", primaryAddr == null ? "<null>" : primaryAddr.getValueString(), extendAddr == null ? "<null>" : extendAddr.getValueString(), addMode == null ? "<null>" : addMode.getValueString());
1647        }
1648
1649        new DccAddressVarHandler(primaryAddr, extendAddr, addMode) {
1650            @Override
1651            protected void doPrimary() {
1652                // short address mode
1653                longMode = false;
1654                if (primaryAddr != null && !primaryAddr.getValueString().equals("")) {
1655                    newAddr = primaryAddr.getValueString();
1656                }
1657            }
1658
1659            @Override
1660            protected void doExtended() {
1661                // long address
1662                if (extendAddr != null && !extendAddr.getValueString().equals("")) {
1663                    longMode = true;
1664                    newAddr = extendAddr.getValueString();
1665                }
1666            }
1667        };
1668        // update if needed
1669        if (newAddr != null) {
1670            // store DCC address, type
1671            _rPane.setDccAddress(newAddr);
1672            _rPane.setDccAddressLong(longMode);
1673        }
1674    }
1675
1676    public void newPane(String name, Element pane, Element modelElem, boolean enableEmpty, boolean programmerPane) {
1677        if (log.isDebugEnabled()) {
1678            log.debug("newPane with enableEmpty {} showEmptyPanes {}", enableEmpty, isShowingEmptyPanes());
1679        }
1680        
1681        // create place-keeper tab
1682        ThreadingUtil.runOnGUI(() -> {
1683            tabPane.addTab(name, makeStandinComponent());
1684        });
1685        
1686        // create a panel to hold columns via separate thread 
1687        final var parent = this;
1688        threadCount.incrementAndGet();
1689        new javax.swing.SwingWorker<PaneProgPane, Object>(){
1690            @Override
1691            public PaneProgPane doInBackground() {
1692               return new PaneProgPane(parent, name, pane, cvModel, variableModel, modelElem, _rosterEntry, programmerPane);
1693            }
1694            @Override
1695            protected void done() {
1696                try {
1697                    var p = get();
1698                    p.setOpaque(true);
1699                    if (noDecoder) {
1700                        p.setNoDecoder();
1701                        cvModel.setNoDecoder();
1702                    }
1703                    // how to handle the tab depends on whether it has contents and option setting
1704                    int index;
1705                    if (enableEmpty || !p.cvList.isEmpty() || !p.varList.isEmpty()) {
1706                        // Was there a race condition here with qualified panes?
1707                        // QualifiedVarTest attempts to invoke that, but haven't it with the following code
1708                        index = tabPane.indexOfTab(name);
1709                        tabPane.setComponentAt(tabPane.indexOfTab(name), p);  // always add if not empty
1710                        tabPane.setToolTipTextAt(tabPane.indexOfTab(name), p.getToolTipText());
1711                    } else if (isShowingEmptyPanes()) {
1712                        // here empty, but showing anyway as disabled
1713                        index = tabPane.indexOfTab(name);
1714                        tabPane.setComponentAt(tabPane.indexOfTab(name), p);
1715                        tabPane.setToolTipTextAt(tabPane.indexOfTab(name),
1716                                Bundle.getMessage("TipTabEmptyNoCategory"));
1717                        tabPane.setEnabledAt(tabPane.indexOfTab(name), true); // need to enable the pane so user can see message
1718                    } else {
1719                        // here not showing tab at all
1720                        index = -1;
1721                        log.trace("deleted {} tab here", name);
1722                        tabPane.removeTabAt(tabPane.indexOfTab(name));
1723                    }
1724            
1725                    // remember it for programming
1726                    paneList.add(p);
1727            
1728                    // if visible, set qualifications
1729                    if (index >= 0) {
1730                        processModifierElements(pane, p, variableModel, tabPane, name);
1731                    }       
1732                    threadCount.decrementAndGet(); 
1733                } catch (InterruptedException | java.util.concurrent.ExecutionException e) {
1734                    log.error("Exception",e);
1735                }
1736            }
1737        }.execute();        
1738        
1739    }
1740
1741    /**
1742     * If there are any modifier elements, process them.
1743     *
1744     * @param e Process the contents of this element
1745     * @param pane Destination of any visible items
1746     * @param model Used to locate any needed variables
1747     * @param tabPane For overall GUI navigation
1748     * @param name Which pane in the overall window
1749     */
1750    protected void processModifierElements(Element e, final PaneProgPane pane, VariableTableModel model, final JTabbedPane tabPane, final String name) {
1751        QualifierAdder qa = new QualifierAdder() {
1752            @Override
1753            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
1754                return new PaneQualifier(pane, var, Integer.parseInt(value), relation, tabPane, name);
1755            }
1756
1757            @Override
1758            protected void addListener(java.beans.PropertyChangeListener qc) {
1759                pane.addPropertyChangeListener(qc);
1760            }
1761        };
1762
1763        qa.processModifierElements(e, model);
1764    }
1765
1766    @Override
1767    public BusyGlassPane getBusyGlassPane() {
1768        return glassPane;
1769    }
1770
1771    /**
1772     * Create a BusyGlassPane transparent layer over the panel blocking any
1773     * other interaction, excluding a supplied button.
1774     *
1775     * @param activeButton a button to put on top of the pane
1776     */
1777    @Override
1778    public void prepGlassPane(AbstractButton activeButton) {
1779        List<Rectangle> rectangles = new ArrayList<>();
1780
1781        if (glassPane != null) {
1782            glassPane.dispose();
1783        }
1784        activeComponents.clear();
1785        activeComponents.add(activeButton);
1786        if (activeButton == readChangesButton || activeButton == readAllButton
1787                || activeButton == writeChangesButton || activeButton == writeAllButton) {
1788            if (activeButton == readChangesButton) {
1789                for (JPanel jPanel : paneList) {
1790                    assert jPanel instanceof PaneProgPane;
1791                    activeComponents.add(((PaneProgPane) jPanel).readChangesButton);
1792                }
1793            } else if (activeButton == readAllButton) {
1794                for (JPanel jPanel : paneList) {
1795                    assert jPanel instanceof PaneProgPane;
1796                    activeComponents.add(((PaneProgPane) jPanel).readAllButton);
1797                }
1798            } else if (activeButton == writeChangesButton) {
1799                for (JPanel jPanel : paneList) {
1800                    assert jPanel instanceof PaneProgPane;
1801                    activeComponents.add(((PaneProgPane) jPanel).writeChangesButton);
1802                }
1803            } else { // (activeButton == writeAllButton) {
1804                for (JPanel jPanel : paneList) {
1805                    assert jPanel instanceof PaneProgPane;
1806                    activeComponents.add(((PaneProgPane) jPanel).writeAllButton);
1807                }
1808            }
1809
1810            for (int i = 0; i < tabPane.getTabCount(); i++) {
1811                rectangles.add(tabPane.getUI().getTabBounds(tabPane, i));
1812            }
1813        }
1814        glassPane = new BusyGlassPane(activeComponents, rectangles, this.getContentPane(), this);
1815        this.setGlassPane(glassPane);
1816    }
1817
1818    @Override
1819    public void paneFinished() {
1820        log.debug("paneFinished with isBusy={}", isBusy());
1821        if (!isBusy()) {
1822            if (glassPane != null) {
1823                glassPane.setVisible(false);
1824                glassPane.dispose();
1825                glassPane = null;
1826            }
1827            setCursor(Cursor.getDefaultCursor());
1828            enableButtons(true);
1829        }
1830    }
1831
1832    /**
1833     * Enable the read/write buttons.
1834     * <p>
1835     * In addition, if a programming mode pane is present, its "set" button is
1836     * enabled.
1837     *
1838     * @param stat Are reads possible? If false, so not enable the read buttons.
1839     */
1840    @Override
1841    public void enableButtons(boolean stat) {
1842        log.debug("enableButtons({})", stat);
1843        if (noDecoder) {
1844            // If we don't have a decoder, no read or write is possible
1845            stat = false;
1846        }
1847        if (stat) {
1848            enableReadButtons();
1849        } else {
1850            readChangesButton.setEnabled(false);
1851            readAllButton.setEnabled(false);
1852        }
1853        writeChangesButton.setEnabled(stat);
1854        writeAllButton.setEnabled(stat);
1855
1856        var tempModePane = getModePane();
1857        if (tempModePane != null) {
1858            tempModePane.setEnabled(stat);
1859        }
1860    }
1861
1862    boolean justChanges;
1863
1864    @Override
1865    public boolean isBusy() {
1866        return _busy;
1867    }
1868    private boolean _busy = false;
1869
1870    private void setBusy(boolean stat) {
1871        log.debug("setBusy({})", stat);
1872        _busy = stat;
1873
1874        for (JPanel jPanel : paneList) {
1875            assert jPanel instanceof PaneProgPane;
1876            ((PaneProgPane) jPanel).enableButtons(!stat);
1877        }
1878        if (!stat) {
1879            paneFinished();
1880        }
1881    }
1882
1883    /**
1884     * Invoked by "Read Changes" button, this sets in motion a continuing
1885     * sequence of "read changes" operations on the panes.
1886     * <p>
1887     * Each invocation of this method reads one pane; completion of that request
1888     * will cause it to happen again, reading the next pane, until there's
1889     * nothing left to read.
1890     *
1891     * @return true if a read has been started, false if the operation is
1892     *         complete.
1893     */
1894    public boolean readChanges() {
1895        log.debug("readChanges starts");
1896        justChanges = true;
1897        for (JPanel jPanel : paneList) {
1898            assert jPanel instanceof PaneProgPane;
1899            ((PaneProgPane) jPanel).setToRead(justChanges, true);
1900        }
1901        setBusy(true);
1902        enableButtons(false);
1903        readChangesButton.setEnabled(true);
1904        glassPane.setVisible(true);
1905        paneListIndex = 0;
1906        // start operation
1907        return doRead();
1908    }
1909
1910    /**
1911     * Invoked by the "Read All" button, this sets in motion a continuing
1912     * sequence of "read all" operations on the panes.
1913     * <p>
1914     * Each invocation of this method reads one pane; completion of that request
1915     * will cause it to happen again, reading the next pane, until there's
1916     * nothing left to read.
1917     *
1918     * @return true if a read has been started, false if the operation is
1919     *         complete.
1920     */
1921    public boolean readAll() {
1922        log.debug("readAll starts");
1923        justChanges = false;
1924        for (JPanel jPanel : paneList) {
1925            assert jPanel instanceof PaneProgPane;
1926            ((PaneProgPane) jPanel).setToRead(justChanges, true);
1927        }
1928        setBusy(true);
1929        enableButtons(false);
1930        readAllButton.setEnabled(true);
1931        glassPane.setVisible(true);
1932        paneListIndex = 0;
1933        // start operation
1934        return doRead();
1935    }
1936
1937    boolean doRead() {
1938        _read = true;
1939        while (paneListIndex < paneList.size()) {
1940            log.debug("doRead on {}", paneListIndex);
1941            _programmingPane = (PaneProgPane) paneList.get(paneListIndex);
1942            // some programming operations are instant, so need to have listener registered at readPaneAll
1943            _programmingPane.addPropertyChangeListener(this);
1944            boolean running;
1945            if (justChanges) {
1946                running = _programmingPane.readPaneChanges();
1947            } else {
1948                running = _programmingPane.readPaneAll();
1949            }
1950
1951            paneListIndex++;
1952
1953            if (running) {
1954                // operation in progress, stop loop until called back
1955                log.debug("doRead expecting callback from readPane {}", paneListIndex);
1956                return true;
1957            } else {
1958                _programmingPane.removePropertyChangeListener(this);
1959            }
1960        }
1961        // nothing to program, end politely
1962        _programmingPane = null;
1963        enableButtons(true);
1964        setBusy(false);
1965        readChangesButton.setSelected(false);
1966        readAllButton.setSelected(false);
1967        log.debug("doRead found nothing to do");
1968        return false;
1969    }
1970
1971    /**
1972     * Invoked by "Write All" button, this sets in motion a continuing sequence
1973     * of "write all" operations on each pane. Each invocation of this method
1974     * writes one pane; completion of that request will cause it to happen
1975     * again, writing the next pane, until there's nothing left to write.
1976     *
1977     * @return true if a write has been started, false if the operation is
1978     *         complete.
1979     */
1980    public boolean writeAll() {
1981        log.debug("writeAll starts");
1982        justChanges = false;
1983        for (JPanel jPanel : paneList) {
1984            assert jPanel instanceof PaneProgPane;
1985            ((PaneProgPane) jPanel).setToWrite(justChanges, true);
1986        }
1987        setBusy(true);
1988        enableButtons(false);
1989        writeAllButton.setEnabled(true);
1990        glassPane.setVisible(true);
1991        paneListIndex = 0;
1992        return doWrite();
1993    }
1994
1995    /**
1996     * Invoked by "Write Changes" button, this sets in motion a continuing
1997     * sequence of "write changes" operations on each pane.
1998     * <p>
1999     * Each invocation of this method writes one pane; completion of that
2000     * request will cause it to happen again, writing the next pane, until
2001     * there's nothing left to write.
2002     *
2003     * @return true if a write has been started, false if the operation is
2004     *         complete
2005     */
2006    public boolean writeChanges() {
2007        log.debug("writeChanges starts");
2008        justChanges = true;
2009        for (JPanel jPanel : paneList) {
2010            assert jPanel instanceof PaneProgPane;
2011            ((PaneProgPane) jPanel).setToWrite(justChanges, true);
2012        }
2013        setBusy(true);
2014        enableButtons(false);
2015        writeChangesButton.setEnabled(true);
2016        glassPane.setVisible(true);
2017        paneListIndex = 0;
2018        return doWrite();
2019    }
2020
2021    boolean doWrite() {
2022        _read = false;
2023        while (paneListIndex < paneList.size()) {
2024            log.debug("doWrite starts on {}", paneListIndex);
2025            _programmingPane = (PaneProgPane) paneList.get(paneListIndex);
2026            // some programming operations are instant, so need to have listener registered at readPane
2027            _programmingPane.addPropertyChangeListener(this);
2028            boolean running;
2029            if (justChanges) {
2030                running = _programmingPane.writePaneChanges();
2031            } else {
2032                running = _programmingPane.writePaneAll();
2033            }
2034
2035            paneListIndex++;
2036
2037            if (running) {
2038                // operation in progress, stop loop until called back
2039                log.debug("doWrite expecting callback from writePane {}", paneListIndex);
2040                return true;
2041            } else {
2042                _programmingPane.removePropertyChangeListener(this);
2043            }
2044        }
2045        // nothing to program, end politely
2046        _programmingPane = null;
2047        enableButtons(true);
2048        setBusy(false);
2049        writeChangesButton.setSelected(false);
2050        writeAllButton.setSelected(false);
2051        log.debug("doWrite found nothing to do");
2052        return false;
2053    }
2054
2055    /**
2056     * Prepare a roster entry to be printed, and display a selection list.
2057     *
2058     * @see jmri.jmrit.roster.PrintRosterEntry#doPrintPanes(boolean)
2059     * @param preview true if output should go to a Preview pane on screen,
2060     *                false to output to a printer (dialog)
2061     */
2062    public void printPanes(final boolean preview) {
2063        PrintRosterEntry pre = new PrintRosterEntry(_rosterEntry, paneList, _flPane, _rMPane, this);
2064        pre.printPanes(preview);
2065    }
2066
2067    boolean _read = true;
2068    PaneProgPane _programmingPane = null;
2069
2070    /**
2071     * Get notification of a variable property change in the pane, specifically
2072     * "busy" going to false at the end of a programming operation.
2073     *
2074     * @param e Event, used to find source
2075     */
2076    @Override
2077    public void propertyChange(java.beans.PropertyChangeEvent e) {
2078        // check for the right event
2079        if (_programmingPane == null) {
2080            log.warn("unexpected propertyChange: {}", e);
2081            return;
2082        } else if (log.isDebugEnabled()) {
2083            log.debug("property changed: {} new value: {}", e.getPropertyName(), e.getNewValue());
2084        }
2085        log.debug("check valid: {} {} {}", e.getSource() == _programmingPane, !e.getPropertyName().equals("Busy"), e.getNewValue().equals(Boolean.FALSE));
2086        if (e.getSource() == _programmingPane
2087                && e.getPropertyName().equals("Busy")
2088                && e.getNewValue().equals(Boolean.FALSE)) {
2089
2090            log.debug("end of a programming pane operation, remove");
2091            // remove existing listener
2092            _programmingPane.removePropertyChangeListener(this);
2093            _programmingPane = null;
2094            // restart the operation
2095            if (_read && readChangesButton.isSelected()) {
2096                log.debug("restart readChanges");
2097                doRead();
2098            } else if (_read && readAllButton.isSelected()) {
2099                log.debug("restart readAll");
2100                doRead();
2101            } else if (writeChangesButton.isSelected()) {
2102                log.debug("restart writeChanges");
2103                doWrite();
2104            } else if (writeAllButton.isSelected()) {
2105                log.debug("restart writeAll");
2106                doWrite();
2107            } else {
2108                log.debug("read/write end because button is lifted");
2109                setBusy(false);
2110            }
2111        }
2112    }
2113
2114    /**
2115     * Store the locomotives information in the roster (and a RosterEntry file).
2116     *
2117     * @return false if store failed
2118     */
2119    @InvokeOnGuiThread
2120    public boolean storeFile() {
2121        log.debug("storeFile starts");
2122
2123        if (_rPane.checkDuplicate()) {
2124            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorDuplicateID"));
2125            return false;
2126        }
2127
2128        // reload the RosterEntry
2129        updateDccAddress();
2130        _rPane.update(_rosterEntry);
2131        _flPane.update(_rosterEntry);
2132        _rMPane.update(_rosterEntry);
2133        if (_physicsPane != null)
2134            _physicsPane.update(_rosterEntry);
2135
2136        // id has to be set!
2137        if (_rosterEntry.getId().equals("") || _rosterEntry.getId().equals(Bundle.getMessage("LabelNewDecoder"))) {
2138            log.debug("storeFile without a filename; issued dialog");
2139            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("PromptFillInID"));
2140            return false;
2141        }
2142
2143        // if there isn't a filename, store using the id
2144        _rosterEntry.ensureFilenameExists();
2145        String filename = _rosterEntry.getFileName();
2146
2147        // do actual file writes in a separate thread, wait for success
2148        threadCount.incrementAndGet();
2149        new javax.swing.SwingWorker<Object, Object>(){
2150            @Override
2151            public Object doInBackground() {
2152                actualFileWrites();
2153                return null;
2154            }
2155            @Override
2156            protected void done() {
2157                // show OK status
2158                progStatus.setText(java.text.MessageFormat.format(
2159                        Bundle.getMessage("StateSaveOK"), filename));
2160                threadCount.decrementAndGet(); 
2161            }
2162        }.execute();
2163
2164        // mark this as a success
2165        variableModel.setFileDirty(false);
2166        maxFnNumDirty = false;
2167
2168        // save date changed, update
2169        _rPane.updateGUI(_rosterEntry);
2170        if (_physicsPane != null) _physicsPane.updateGUI(_rosterEntry);
2171
2172        return true;
2173    }
2174
2175    @InvokeOnAnyThread
2176    private void actualFileWrites() {
2177        // create the RosterEntry to its file
2178        _rosterEntry.writeFile(cvModel, variableModel);
2179
2180        // and store an updated roster file
2181        FileUtil.createDirectory(FileUtil.getUserFilesPath());
2182        Roster.getDefault().writeRoster();
2183    }
2184    
2185    /**
2186     * Local dispose, which also invokes parent. Note that we remove the
2187     * components (removeAll) before taking those apart.
2188     */
2189    @OverridingMethodsMustInvokeSuper
2190    @Override
2191    public void dispose() {
2192        log.debug("dispose local");
2193
2194        // remove listeners (not much of a point, though)
2195        readChangesButton.removeItemListener(l1);
2196        writeChangesButton.removeItemListener(l2);
2197        readAllButton.removeItemListener(l3);
2198        writeAllButton.removeItemListener(l4);
2199        if (_programmingPane != null) {
2200            _programmingPane.removePropertyChangeListener(this);
2201        }
2202
2203        // dispose the list of panes
2204        //noinspection ForLoopReplaceableByForEach
2205        for (int i = 0; i < paneList.size(); i++) {
2206            PaneProgPane p = (PaneProgPane) paneList.get(i);
2207            tabPane.remove(p);
2208            p.dispose();
2209        }
2210        paneList.clear();
2211
2212        // dispose of things we owned, in order of dependence
2213        // null checks are needed for (partial) testing
2214        if (_rPane != null) _rPane.dispose();
2215        if (_flPane != null) _flPane.dispose();
2216        if (_rMPane != null) _rMPane.dispose();
2217        if (variableModel != null) variableModel.dispose();
2218        if (cvModel != null) cvModel.dispose();
2219        if (_rosterEntry != null) {
2220            _rosterEntry.setOpen(false);
2221        }
2222
2223        // remove references to everything we remember
2224        progStatus = null;
2225        cvModel = null;
2226        variableModel = null;
2227        _rosterEntry = null;
2228        _rPane = null;
2229        _flPane = null;
2230        _rMPane = null;
2231
2232        paneList.clear();
2233        paneList = null;
2234        _programmingPane = null;
2235
2236        tabPane = null;
2237        readChangesButton = null;
2238        writeChangesButton = null;
2239        readAllButton = null;
2240        writeAllButton = null;
2241
2242        log.debug("dispose superclass");
2243        removeAll();
2244        super.dispose();
2245    }
2246
2247    /**
2248     * Set value of Preference option to show empty panes.
2249     *
2250     * @param yes true if empty panes should be shown
2251     */
2252    public static void setShowEmptyPanes(boolean yes) {
2253        if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) {
2254            InstanceManager.getDefault(ProgrammerConfigManager.class).setShowEmptyPanes(yes);
2255        }
2256    }
2257
2258    /**
2259     * Get value of Preference option to show empty panes.
2260     *
2261     * @return value from programmer config. manager, else true.
2262     */
2263    public static boolean getShowEmptyPanes() {
2264        return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null ||
2265                InstanceManager.getDefault(ProgrammerConfigManager.class).isShowEmptyPanes();
2266    }
2267
2268    /**
2269     * Get value of whether current item should show empty panes.
2270     */
2271    private boolean isShowingEmptyPanes() {
2272        boolean temp = getShowEmptyPanes();
2273        if (programmerShowEmptyPanes.equals("yes")) {
2274            temp = true;
2275        } else if (programmerShowEmptyPanes.equals("no")) {
2276            temp = false;
2277        }
2278        if (decoderShowEmptyPanes.equals("yes")) {
2279            temp = true;
2280        } else if (decoderShowEmptyPanes.equals("no")) {
2281            temp = false;
2282        }
2283        return temp;
2284    }
2285
2286    /**
2287     * Option to control appearance of CV numbers in tool tips.
2288     *
2289     * @param yes true is CV numbers should be shown
2290     */
2291    public static void setShowCvNumbers(boolean yes) {
2292        if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) {
2293            InstanceManager.getDefault(ProgrammerConfigManager.class).setShowCvNumbers(yes);
2294        }
2295    }
2296
2297    public static boolean getShowCvNumbers() {
2298        return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null ||
2299                InstanceManager.getDefault(ProgrammerConfigManager.class).isShowCvNumbers();
2300    }
2301
2302    public static void setCanCacheDefault(boolean yes) {
2303        if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) {
2304            InstanceManager.getDefault(ProgrammerConfigManager.class).setCanCacheDefault(yes);
2305        }
2306    }
2307
2308    public static boolean getCanCacheDefault() {
2309        return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null ||
2310                InstanceManager.getDefault(ProgrammerConfigManager.class).isCanCacheDefault();
2311    }
2312
2313    public static void setDoConfirmRead(boolean yes) {
2314        if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) {
2315            InstanceManager.getDefault(ProgrammerConfigManager.class).setDoConfirmRead(yes);
2316        }
2317    }
2318
2319    public static boolean getDoConfirmRead() {
2320        return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null ||
2321                InstanceManager.getDefault(ProgrammerConfigManager.class).isDoConfirmRead();
2322    }
2323
2324    public static void setDisableProgrammingTrack(boolean yes) {
2325        if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) {
2326            InstanceManager.getDefault(ProgrammerConfigManager.class).setDisableProgrammingTrack(yes);
2327        }
2328    }
2329
2330    public static boolean getDisableProgrammingTrack() {
2331        return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null ||
2332                InstanceManager.getDefault(ProgrammerConfigManager.class).isDisableProgrammingTrack();
2333    }
2334
2335    public static void setDisableProgrammingOnMain(boolean yes) {
2336        if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) {
2337            InstanceManager.getDefault(ProgrammerConfigManager.class).setDisableProgrammingOnMain(yes);
2338        }
2339    }
2340
2341    public static boolean getDisableProgrammingOnMain() {
2342        return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null ||
2343                InstanceManager.getDefault(ProgrammerConfigManager.class).isDisableProgrammingOnMain();
2344    }
2345
2346    public RosterEntry getRosterEntry() {
2347        return _rosterEntry;
2348    }
2349
2350    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(PaneProgFrame.class);
2351
2352}