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