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