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