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