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