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