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        // add the comment tab
1198        JPanel commentTab = new JPanel();
1199        var comment = new JTextArea(_rPane.getCommentDocument());
1200        JScrollPane commentScroller = new JScrollPane(comment, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
1201        commentTab.add(commentScroller);
1202        commentTab.setLayout(new BoxLayout(commentTab, BoxLayout.Y_AXIS));
1203        tabPane.addTab(Bundle.getMessage("COMMENT PANE"), commentTab);
1204
1205        // for all "pane" elements in the programmer
1206        List<Element> progPaneList = base.getChildren("pane");
1207        if (log.isDebugEnabled()) {
1208            log.debug("will process {} pane definitions", progPaneList.size());
1209        }
1210        for (Element temp : progPaneList) {
1211            // load each programmer pane
1212            List<Element> pnames = temp.getChildren("name");
1213            boolean isProgPane = true;
1214            if ((pnames.size() > 0) && (decoderPaneList != null) && (decoderPaneList.size() > 0)) {
1215                String namePrimary = (pnames.get(0)).getValue(); // get non-localised name
1216
1217                // check if there is a same-name pane in decoder file
1218                // start at end to prevent concurrentmodification exception on remove
1219                for (int j = decoderPaneList.size() - 1; j >= 0; j--) {
1220                    List<Element> dnames = decoderPaneList.get(j).getChildren("name");
1221                    if (dnames.size() > 0) {
1222                        String namePrimaryDecoder = (dnames.get(0)).getValue(); // get non-localised name
1223                        if (namePrimary.equals(namePrimaryDecoder)) {
1224                            // replace programmer pane with same-name decoder pane
1225                            temp = decoderPaneList.get(j);
1226                            decoderPaneList.remove(j); // safe, not suspicious as we work end - front
1227                            isProgPane = false;
1228                        }
1229                    }
1230                }
1231            }
1232            String name = jmri.util.jdom.LocaleSelector.getAttribute(temp, "name");
1233
1234            // handle include/exclude
1235            if (isIncludedFE(temp, modelElem, _rosterEntry, "", "")) {
1236                newPane(name, temp, modelElem, false, isProgPane);  // don't force showing if empty
1237                log.debug("readConfig - pane {} added", name); // these are also in RosterPrint
1238            }
1239        }
1240    }
1241
1242    /**
1243     * Reset all CV values to defaults stored earlier.
1244     * <p>
1245     * This will in turn update the variables.
1246     */
1247    protected void resetToDefaults() {
1248        int n = defaultCvValues.length;
1249        for (int i = 0; i < n; i++) {
1250            CvValue cv = cvModel.getCvByNumber(defaultCvNumbers[i]);
1251            if (cv == null) {
1252                log.warn("Trying to set default in CV {} but didn't find the CV object", defaultCvNumbers[i]);
1253            } else {
1254                cv.setValue(defaultCvValues[i]);
1255            }
1256        }
1257    }
1258
1259    int[] defaultCvValues = null;
1260    String[] defaultCvNumbers = null;
1261
1262    /**
1263     * Save all CV values.
1264     * <p>
1265     * These stored values are used by {link #resetToDefaults()}
1266     */
1267    protected void saveDefaults() {
1268        int n = cvModel.getRowCount();
1269        defaultCvValues = new int[n];
1270        defaultCvNumbers = new String[n];
1271
1272        for (int i = 0; i < n; i++) {
1273            CvValue cv = cvModel.getCvByRow(i);
1274            defaultCvValues[i] = cv.getValue();
1275            defaultCvNumbers[i] = cv.number();
1276        }
1277    }
1278
1279    protected JPanel makeInfoPane(RosterEntry r) {
1280        // create the identification pane (not configured by programmer file now; maybe later?)
1281
1282        JPanel outer = new JPanel();
1283        outer.setLayout(new BoxLayout(outer, BoxLayout.Y_AXIS));
1284        JPanel body = new JPanel();
1285        body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS));
1286        JScrollPane scrollPane = new JScrollPane(body);
1287
1288        // add roster info
1289        _rPane = new RosterEntryPane(r);
1290        _rPane.setMaximumSize(_rPane.getPreferredSize());
1291        body.add(_rPane);
1292
1293        // add the store button
1294        JButton store = new JButton(Bundle.getMessage("ButtonSave"));
1295        store.setAlignmentX(JLabel.CENTER_ALIGNMENT);
1296        store.addActionListener(e -> storeFile());
1297
1298        // add the reset button
1299        JButton reset = new JButton(Bundle.getMessage("ButtonResetDefaults"));
1300        reset.setAlignmentX(JLabel.CENTER_ALIGNMENT);
1301        if (decoderAllowResetDefaults.equals("no")) {
1302            reset.setEnabled(false);
1303            reset.setToolTipText(Bundle.getMessage("TipButtonResetDefaultsDisabled"));
1304        } else {
1305            reset.setToolTipText(Bundle.getMessage("TipButtonResetDefaults"));
1306            reset.addActionListener(e -> resetToDefaults());
1307        }
1308
1309        int sizeX = Math.max(reset.getPreferredSize().width, store.getPreferredSize().width);
1310        int sizeY = Math.max(reset.getPreferredSize().height, store.getPreferredSize().height);
1311        store.setPreferredSize(new Dimension(sizeX, sizeY));
1312        reset.setPreferredSize(new Dimension(sizeX, sizeY));
1313
1314        store.setToolTipText(_rosterEntry.getFileName());
1315
1316        JPanel buttons = new JPanel();
1317        buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
1318
1319        buttons.add(store);
1320        buttons.add(reset);
1321
1322        body.add(buttons);
1323        outer.add(scrollPane);
1324
1325        // arrange for the dcc address to be updated
1326        java.beans.PropertyChangeListener dccNews = e -> updateDccAddress();
1327        primaryAddr = variableModel.findVar("Short Address");
1328        if (primaryAddr == null) {
1329            log.debug("DCC Address monitor didn't find a Short Address variable");
1330        } else {
1331            primaryAddr.addPropertyChangeListener(dccNews);
1332        }
1333        extendAddr = variableModel.findVar("Long Address");
1334        if (extendAddr == null) {
1335            log.debug("DCC Address monitor didn't find an Long Address variable");
1336        } else {
1337            extendAddr.addPropertyChangeListener(dccNews);
1338        }
1339        addMode = (EnumVariableValue) variableModel.findVar("Address Format");
1340        if (addMode == null) {
1341            log.debug("DCC Address monitor didn't find an Address Format variable");
1342        } else {
1343            addMode.addPropertyChangeListener(dccNews);
1344        }
1345
1346        // get right address to start
1347        updateDccAddress();
1348
1349        return outer;
1350    }
1351
1352    protected JPanel makeFunctionLabelPane(RosterEntry r) {
1353        // create the identification pane (not configured by programmer file now; maybe later?)
1354
1355        JPanel outer = new JPanel();
1356        outer.setLayout(new BoxLayout(outer, BoxLayout.Y_AXIS));
1357        JPanel body = new JPanel();
1358        body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS));
1359        JScrollPane scrollPane = new JScrollPane(body);
1360
1361        // add tab description
1362        JLabel title = new JLabel(Bundle.getMessage("UseThisTabCustomize"));
1363        title.setAlignmentX(JLabel.CENTER_ALIGNMENT);
1364        body.add(title);
1365        body.add(new JLabel(" ")); // some padding
1366
1367        // add roster info
1368        _flPane = new FunctionLabelPane(r);
1369        //_flPane.setMaximumSize(_flPane.getPreferredSize());
1370        body.add(_flPane);
1371
1372        // add the store button
1373        JButton store = new JButton(Bundle.getMessage("ButtonSave"));
1374        store.setAlignmentX(JLabel.CENTER_ALIGNMENT);
1375        store.addActionListener(e -> storeFile());
1376
1377        store.setToolTipText(_rosterEntry.getFileName());
1378
1379        JPanel buttons = new JPanel();
1380        buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
1381
1382        buttons.add(store);
1383
1384        body.add(buttons);
1385        outer.add(scrollPane);
1386        return outer;
1387    }
1388
1389    protected JPanel makeMediaPane(RosterEntry r) {
1390        // create the identification pane (not configured by programmer file now; maybe later?)
1391        JPanel outer = new JPanel();
1392        outer.setLayout(new BoxLayout(outer, BoxLayout.Y_AXIS));
1393        JPanel body = new JPanel();
1394        body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS));
1395        JScrollPane scrollPane = new JScrollPane(body);
1396
1397        // add tab description
1398        JLabel title = new JLabel(Bundle.getMessage("UseThisTabMedia"));
1399        title.setAlignmentX(JLabel.CENTER_ALIGNMENT);
1400        body.add(title);
1401        body.add(new JLabel(" ")); // some padding
1402
1403        // add roster info
1404        _rMPane = new RosterMediaPane(r);
1405        _rMPane.setMaximumSize(_rMPane.getPreferredSize());
1406        body.add(_rMPane);
1407
1408        // add the store button
1409        JButton store = new JButton(Bundle.getMessage("ButtonSave"));
1410        store.setAlignmentX(JLabel.CENTER_ALIGNMENT);
1411        store.addActionListener(e -> storeFile());
1412
1413        JPanel buttons = new JPanel();
1414        buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
1415
1416        buttons.add(store);
1417
1418        body.add(buttons);
1419        outer.add(scrollPane);
1420        return outer;
1421    }
1422
1423    // hold refs to variables to check dccAddress
1424    VariableValue primaryAddr = null;
1425    VariableValue extendAddr = null;
1426    EnumVariableValue addMode = null;
1427
1428    boolean longMode = false;
1429    String newAddr = null;
1430
1431    void updateDccAddress() {
1432
1433        if (log.isDebugEnabled()) {
1434            log.debug("updateDccAddress: short {} long {} mode {}", primaryAddr == null ? "<null>" : primaryAddr.getValueString(), extendAddr == null ? "<null>" : extendAddr.getValueString(), addMode == null ? "<null>" : addMode.getValueString());
1435        }
1436
1437        new DccAddressVarHandler(primaryAddr, extendAddr, addMode) {
1438            @Override
1439            protected void doPrimary() {
1440                // short address mode
1441                longMode = false;
1442                if (primaryAddr != null && !primaryAddr.getValueString().equals("")) {
1443                    newAddr = primaryAddr.getValueString();
1444                }
1445            }
1446
1447            @Override
1448            protected void doExtended() {
1449                // long address
1450                if (extendAddr != null && !extendAddr.getValueString().equals("")) {
1451                    longMode = true;
1452                    newAddr = extendAddr.getValueString();
1453                }
1454            }
1455        };
1456        // update if needed
1457        if (newAddr != null) {
1458            // store DCC address, type
1459            _rPane.setDccAddress(newAddr);
1460            _rPane.setDccAddressLong(longMode);
1461        }
1462    }
1463
1464    public void newPane(String name, Element pane, Element modelElem, boolean enableEmpty, boolean programmerPane) {
1465        if (log.isDebugEnabled()) {
1466            log.debug("newPane with enableEmpty {} showEmptyPanes {}", enableEmpty, isShowingEmptyPanes());
1467        }
1468        // create a panel to hold columns
1469        PaneProgPane p = new PaneProgPane(this, name, pane, cvModel, variableModel, modelElem, _rosterEntry, programmerPane);
1470        p.setOpaque(true);
1471        if (noDecoder) {
1472            p.setNoDecoder();
1473            cvModel.setNoDecoder();
1474        }
1475        // how to handle the tab depends on whether it has contents and option setting
1476        int index;
1477        if (enableEmpty || !p.cvList.isEmpty() || !p.varList.isEmpty()) {
1478            tabPane.addTab(name, p);  // always add if not empty
1479            index = tabPane.indexOfTab(name);
1480            tabPane.setToolTipTextAt(index, p.getToolTipText());
1481        } else if (isShowingEmptyPanes()) {
1482            // here empty, but showing anyway as disabled
1483            tabPane.addTab(name, p);
1484            index = tabPane.indexOfTab(name);
1485            tabPane.setEnabledAt(index, true); // need to enable the pane so user can see message
1486            tabPane.setToolTipTextAt(index,
1487                    Bundle.getMessage("TipTabEmptyNoCategory"));
1488        } else {
1489            // here not showing tab at all
1490            index = -1;
1491        }
1492
1493        // remember it for programming
1494        paneList.add(p);
1495
1496        // if visible, set qualifications
1497        if (index >= 0) {
1498            processModifierElements(pane, p, variableModel, tabPane, index);
1499        }
1500    }
1501
1502    /**
1503     * If there are any modifier elements, process them.
1504     *
1505     * @param e Process the contents of this element
1506     * @param pane Destination of any visible items
1507     * @param model Used to locate any needed variables
1508     * @param tabPane For overall GUI navigation
1509     * @param index Which pane in the overall window
1510     */
1511    protected void processModifierElements(Element e, final PaneProgPane pane, VariableTableModel model, final JTabbedPane tabPane, final int index) {
1512        QualifierAdder qa = new QualifierAdder() {
1513            @Override
1514            protected Qualifier createQualifier(VariableValue var, String relation, String value) {
1515                return new PaneQualifier(pane, var, Integer.parseInt(value), relation, tabPane, index);
1516            }
1517
1518            @Override
1519            protected void addListener(java.beans.PropertyChangeListener qc) {
1520                pane.addPropertyChangeListener(qc);
1521            }
1522        };
1523
1524        qa.processModifierElements(e, model);
1525    }
1526
1527    @Override
1528    public BusyGlassPane getBusyGlassPane() {
1529        return glassPane;
1530    }
1531
1532    /**
1533     * Create a BusyGlassPane transparent layer over the panel blocking any
1534     * other interaction, excluding a supplied button.
1535     *
1536     * @param activeButton a button to put on top of the pane
1537     */
1538    @Override
1539    public void prepGlassPane(AbstractButton activeButton) {
1540        List<Rectangle> rectangles = new ArrayList<>();
1541
1542        if (glassPane != null) {
1543            glassPane.dispose();
1544        }
1545        activeComponents.clear();
1546        activeComponents.add(activeButton);
1547        if (activeButton == readChangesButton || activeButton == readAllButton
1548                || activeButton == writeChangesButton || activeButton == writeAllButton) {
1549            if (activeButton == readChangesButton) {
1550                for (JPanel jPanel : paneList) {
1551                    assert jPanel instanceof PaneProgPane;
1552                    activeComponents.add(((PaneProgPane) jPanel).readChangesButton);
1553                }
1554            } else if (activeButton == readAllButton) {
1555                for (JPanel jPanel : paneList) {
1556                    assert jPanel instanceof PaneProgPane;
1557                    activeComponents.add(((PaneProgPane) jPanel).readAllButton);
1558                }
1559            } else if (activeButton == writeChangesButton) {
1560                for (JPanel jPanel : paneList) {
1561                    assert jPanel instanceof PaneProgPane;
1562                    activeComponents.add(((PaneProgPane) jPanel).writeChangesButton);
1563                }
1564            } else { // (activeButton == writeAllButton) {
1565                for (JPanel jPanel : paneList) {
1566                    assert jPanel instanceof PaneProgPane;
1567                    activeComponents.add(((PaneProgPane) jPanel).writeAllButton);
1568                }
1569            }
1570
1571            for (int i = 0; i < tabPane.getTabCount(); i++) {
1572                rectangles.add(tabPane.getUI().getTabBounds(tabPane, i));
1573            }
1574        }
1575        glassPane = new BusyGlassPane(activeComponents, rectangles, this.getContentPane(), this);
1576        this.setGlassPane(glassPane);
1577    }
1578
1579    @Override
1580    public void paneFinished() {
1581        log.debug("paneFinished with isBusy={}", isBusy());
1582        if (!isBusy()) {
1583            if (glassPane != null) {
1584                glassPane.setVisible(false);
1585                glassPane.dispose();
1586                glassPane = null;
1587            }
1588            setCursor(Cursor.getDefaultCursor());
1589            enableButtons(true);
1590        }
1591    }
1592
1593    /**
1594     * Enable the read/write buttons.
1595     * <p>
1596     * In addition, if a programming mode pane is present, its "set" button is
1597     * enabled.
1598     *
1599     * @param stat Are reads possible? If false, so not enable the read buttons.
1600     */
1601    @Override
1602    public void enableButtons(boolean stat) {
1603        log.debug("enableButtons({})", stat);
1604        if (noDecoder) {
1605            // If we don't have a decoder, no read or write is possible
1606            stat = false;
1607        }
1608        if (stat) {
1609            enableReadButtons();
1610        } else {
1611            readChangesButton.setEnabled(false);
1612            readAllButton.setEnabled(false);
1613        }
1614        writeChangesButton.setEnabled(stat);
1615        writeAllButton.setEnabled(stat);
1616        
1617        var tempModePane = getModePane();
1618        if (tempModePane != null) {
1619            tempModePane.setEnabled(stat);
1620        }
1621    }
1622
1623    boolean justChanges;
1624
1625    @Override
1626    public boolean isBusy() {
1627        return _busy;
1628    }
1629    private boolean _busy = false;
1630
1631    private void setBusy(boolean stat) {
1632        log.debug("setBusy({})", stat);
1633        _busy = stat;
1634
1635        for (JPanel jPanel : paneList) {
1636            assert jPanel instanceof PaneProgPane;
1637            ((PaneProgPane) jPanel).enableButtons(!stat);
1638        }
1639        if (!stat) {
1640            paneFinished();
1641        }
1642    }
1643
1644    /**
1645     * Invoked by "Read Changes" button, this sets in motion a continuing
1646     * sequence of "read changes" operations on the panes.
1647     * <p>
1648     * Each invocation of this method reads one pane; completion of that request
1649     * will cause it to happen again, reading the next pane, until there's
1650     * nothing left to read.
1651     *
1652     * @return true if a read has been started, false if the operation is
1653     *         complete.
1654     */
1655    public boolean readChanges() {
1656        log.debug("readChanges starts");
1657        justChanges = true;
1658        for (JPanel jPanel : paneList) {
1659            assert jPanel instanceof PaneProgPane;
1660            ((PaneProgPane) jPanel).setToRead(justChanges, true);
1661        }
1662        setBusy(true);
1663        enableButtons(false);
1664        readChangesButton.setEnabled(true);
1665        glassPane.setVisible(true);
1666        paneListIndex = 0;
1667        // start operation
1668        return doRead();
1669    }
1670
1671    /**
1672     * Invoked by the "Read All" button, this sets in motion a continuing
1673     * sequence of "read all" operations on the panes.
1674     * <p>
1675     * Each invocation of this method reads one pane; completion of that request
1676     * will cause it to happen again, reading the next pane, until there's
1677     * nothing left to read.
1678     *
1679     * @return true if a read has been started, false if the operation is
1680     *         complete.
1681     */
1682    public boolean readAll() {
1683        log.debug("readAll starts");
1684        justChanges = false;
1685        for (JPanel jPanel : paneList) {
1686            assert jPanel instanceof PaneProgPane;
1687            ((PaneProgPane) jPanel).setToRead(justChanges, true);
1688        }
1689        setBusy(true);
1690        enableButtons(false);
1691        readAllButton.setEnabled(true);
1692        glassPane.setVisible(true);
1693        paneListIndex = 0;
1694        // start operation
1695        return doRead();
1696    }
1697
1698    boolean doRead() {
1699        _read = true;
1700        while (paneListIndex < paneList.size()) {
1701            log.debug("doRead on {}", paneListIndex);
1702            _programmingPane = (PaneProgPane) paneList.get(paneListIndex);
1703            // some programming operations are instant, so need to have listener registered at readPaneAll
1704            _programmingPane.addPropertyChangeListener(this);
1705            boolean running;
1706            if (justChanges) {
1707                running = _programmingPane.readPaneChanges();
1708            } else {
1709                running = _programmingPane.readPaneAll();
1710            }
1711
1712            paneListIndex++;
1713
1714            if (running) {
1715                // operation in progress, stop loop until called back
1716                log.debug("doRead expecting callback from readPane {}", paneListIndex);
1717                return true;
1718            } else {
1719                _programmingPane.removePropertyChangeListener(this);
1720            }
1721        }
1722        // nothing to program, end politely
1723        _programmingPane = null;
1724        enableButtons(true);
1725        setBusy(false);
1726        readChangesButton.setSelected(false);
1727        readAllButton.setSelected(false);
1728        log.debug("doRead found nothing to do");
1729        return false;
1730    }
1731
1732    /**
1733     * Invoked by "Write All" button, this sets in motion a continuing sequence
1734     * of "write all" operations on each pane. Each invocation of this method
1735     * writes one pane; completion of that request will cause it to happen
1736     * again, writing the next pane, until there's nothing left to write.
1737     *
1738     * @return true if a write has been started, false if the operation is
1739     *         complete.
1740     */
1741    public boolean writeAll() {
1742        log.debug("writeAll starts");
1743        justChanges = false;
1744        for (JPanel jPanel : paneList) {
1745            assert jPanel instanceof PaneProgPane;
1746            ((PaneProgPane) jPanel).setToWrite(justChanges, true);
1747        }
1748        setBusy(true);
1749        enableButtons(false);
1750        writeAllButton.setEnabled(true);
1751        glassPane.setVisible(true);
1752        paneListIndex = 0;
1753        return doWrite();
1754    }
1755
1756    /**
1757     * Invoked by "Write Changes" button, this sets in motion a continuing
1758     * sequence of "write changes" operations on each pane.
1759     * <p>
1760     * Each invocation of this method writes one pane; completion of that
1761     * request will cause it to happen again, writing the next pane, until
1762     * there's nothing left to write.
1763     *
1764     * @return true if a write has been started, false if the operation is
1765     *         complete
1766     */
1767    public boolean writeChanges() {
1768        log.debug("writeChanges starts");
1769        justChanges = true;
1770        for (JPanel jPanel : paneList) {
1771            assert jPanel instanceof PaneProgPane;
1772            ((PaneProgPane) jPanel).setToWrite(justChanges, true);
1773        }
1774        setBusy(true);
1775        enableButtons(false);
1776        writeChangesButton.setEnabled(true);
1777        glassPane.setVisible(true);
1778        paneListIndex = 0;
1779        return doWrite();
1780    }
1781
1782    boolean doWrite() {
1783        _read = false;
1784        while (paneListIndex < paneList.size()) {
1785            log.debug("doWrite starts on {}", paneListIndex);
1786            _programmingPane = (PaneProgPane) paneList.get(paneListIndex);
1787            // some programming operations are instant, so need to have listener registered at readPane
1788            _programmingPane.addPropertyChangeListener(this);
1789            boolean running;
1790            if (justChanges) {
1791                running = _programmingPane.writePaneChanges();
1792            } else {
1793                running = _programmingPane.writePaneAll();
1794            }
1795
1796            paneListIndex++;
1797
1798            if (running) {
1799                // operation in progress, stop loop until called back
1800                log.debug("doWrite expecting callback from writePane {}", paneListIndex);
1801                return true;
1802            } else {
1803                _programmingPane.removePropertyChangeListener(this);
1804            }
1805        }
1806        // nothing to program, end politely
1807        _programmingPane = null;
1808        enableButtons(true);
1809        setBusy(false);
1810        writeChangesButton.setSelected(false);
1811        writeAllButton.setSelected(false);
1812        log.debug("doWrite found nothing to do");
1813        return false;
1814    }
1815
1816    /**
1817     * Prepare a roster entry to be printed, and display a selection list.
1818     *
1819     * @see jmri.jmrit.roster.PrintRosterEntry#doPrintPanes(boolean)
1820     * @param preview true if output should go to a Preview pane on screen,
1821     *                false to output to a printer (dialog)
1822     */
1823    public void printPanes(final boolean preview) {
1824        PrintRosterEntry pre = new PrintRosterEntry(_rosterEntry, paneList, _flPane, _rMPane, this);
1825        pre.printPanes(preview);
1826    }
1827
1828    boolean _read = true;
1829    PaneProgPane _programmingPane = null;
1830
1831    /**
1832     * Get notification of a variable property change in the pane, specifically
1833     * "busy" going to false at the end of a programming operation.
1834     *
1835     * @param e Event, used to find source
1836     */
1837    @Override
1838    public void propertyChange(java.beans.PropertyChangeEvent e) {
1839        // check for the right event
1840        if (_programmingPane == null) {
1841            log.warn("unexpected propertyChange: {}", e);
1842            return;
1843        } else if (log.isDebugEnabled()) {
1844            log.debug("property changed: {} new value: {}", e.getPropertyName(), e.getNewValue());
1845        }
1846        log.debug("check valid: {} {} {}", e.getSource() == _programmingPane, !e.getPropertyName().equals("Busy"), e.getNewValue().equals(Boolean.FALSE));
1847        if (e.getSource() == _programmingPane
1848                && e.getPropertyName().equals("Busy")
1849                && e.getNewValue().equals(Boolean.FALSE)) {
1850
1851            log.debug("end of a programming pane operation, remove");
1852            // remove existing listener
1853            _programmingPane.removePropertyChangeListener(this);
1854            _programmingPane = null;
1855            // restart the operation
1856            if (_read && readChangesButton.isSelected()) {
1857                log.debug("restart readChanges");
1858                doRead();
1859            } else if (_read && readAllButton.isSelected()) {
1860                log.debug("restart readAll");
1861                doRead();
1862            } else if (writeChangesButton.isSelected()) {
1863                log.debug("restart writeChanges");
1864                doWrite();
1865            } else if (writeAllButton.isSelected()) {
1866                log.debug("restart writeAll");
1867                doWrite();
1868            } else {
1869                log.debug("read/write end because button is lifted");
1870                setBusy(false);
1871            }
1872        }
1873    }
1874
1875    /**
1876     * Store the locomotives information in the roster (and a RosterEntry file).
1877     *
1878     * @return false if store failed
1879     */
1880    public boolean storeFile() {
1881        log.debug("storeFile starts");
1882
1883        if (_rPane.checkDuplicate()) {
1884            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorDuplicateID"));
1885            return false;
1886        }
1887
1888        // reload the RosterEntry
1889        updateDccAddress();
1890        _rPane.update(_rosterEntry);
1891        _flPane.update(_rosterEntry);
1892        _rMPane.update(_rosterEntry);
1893
1894        // id has to be set!
1895        if (_rosterEntry.getId().equals("") || _rosterEntry.getId().equals(Bundle.getMessage("LabelNewDecoder"))) {
1896            log.debug("storeFile without a filename; issued dialog");
1897            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("PromptFillInID"));
1898            return false;
1899        }
1900
1901        // if there isn't a filename, store using the id
1902        _rosterEntry.ensureFilenameExists();
1903        String filename = _rosterEntry.getFileName();
1904
1905        // create the RosterEntry to its file
1906        _rosterEntry.writeFile(cvModel, variableModel);
1907
1908        // mark this as a success
1909        variableModel.setFileDirty(false);
1910        maxFnNumDirty = false;
1911
1912        // and store an updated roster file
1913        FileUtil.createDirectory(FileUtil.getUserFilesPath());
1914        Roster.getDefault().writeRoster();
1915
1916        // save date changed, update
1917        _rPane.updateGUI(_rosterEntry);
1918
1919        // show OK status
1920        progStatus.setText(java.text.MessageFormat.format(
1921                Bundle.getMessage("StateSaveOK"), filename));
1922        return true;
1923    }
1924
1925    /**
1926     * Local dispose, which also invokes parent. Note that we remove the
1927     * components (removeAll) before taking those apart.
1928     */
1929    @Override
1930    public void dispose() {
1931        log.debug("dispose local");
1932
1933        // remove listeners (not much of a point, though)
1934        readChangesButton.removeItemListener(l1);
1935        writeChangesButton.removeItemListener(l2);
1936        readAllButton.removeItemListener(l3);
1937        writeAllButton.removeItemListener(l4);
1938        if (_programmingPane != null) {
1939            _programmingPane.removePropertyChangeListener(this);
1940        }
1941
1942        // dispose the list of panes
1943        //noinspection ForLoopReplaceableByForEach
1944        for (int i = 0; i < paneList.size(); i++) {
1945            PaneProgPane p = (PaneProgPane) paneList.get(i);
1946            tabPane.remove(p);
1947            p.dispose();
1948        }
1949        paneList.clear();
1950
1951        // dispose of things we owned, in order of dependence
1952        _rPane.dispose();
1953        _flPane.dispose();
1954        _rMPane.dispose();
1955        variableModel.dispose();
1956        cvModel.dispose();
1957        if (_rosterEntry != null) {
1958            _rosterEntry.setOpen(false);
1959        }
1960
1961        // remove references to everything we remember
1962        progStatus = null;
1963        cvModel = null;
1964        variableModel = null;
1965        _rosterEntry = null;
1966        _rPane = null;
1967        _flPane = null;
1968        _rMPane = null;
1969
1970        paneList.clear();
1971        paneList = null;
1972        _programmingPane = null;
1973
1974        tabPane = null;
1975        readChangesButton = null;
1976        writeChangesButton = null;
1977        readAllButton = null;
1978        writeAllButton = null;
1979
1980        log.debug("dispose superclass");
1981        removeAll();
1982        super.dispose();
1983    }
1984
1985    /**
1986     * Set value of Preference option to show empty panes.
1987     *
1988     * @param yes true if empty panes should be shown
1989     */
1990    public static void setShowEmptyPanes(boolean yes) {
1991        if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) {
1992            InstanceManager.getDefault(ProgrammerConfigManager.class).setShowEmptyPanes(yes);
1993        }
1994    }
1995
1996    /**
1997     * Get value of Preference option to show empty panes.
1998     *
1999     * @return value from programmer config. manager, else true.
2000     */
2001    public static boolean getShowEmptyPanes() {
2002        return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null ||
2003                InstanceManager.getDefault(ProgrammerConfigManager.class).isShowEmptyPanes();
2004    }
2005
2006    /**
2007     * Get value of whether current item should show empty panes.
2008     */
2009    private boolean isShowingEmptyPanes() {
2010        boolean temp = getShowEmptyPanes();
2011        if (programmerShowEmptyPanes.equals("yes")) {
2012            temp = true;
2013        } else if (programmerShowEmptyPanes.equals("no")) {
2014            temp = false;
2015        }
2016        if (decoderShowEmptyPanes.equals("yes")) {
2017            temp = true;
2018        } else if (decoderShowEmptyPanes.equals("no")) {
2019            temp = false;
2020        }
2021        return temp;
2022    }
2023
2024    /**
2025     * Option to control appearance of CV numbers in tool tips.
2026     *
2027     * @param yes true is CV numbers should be shown
2028     */
2029    public static void setShowCvNumbers(boolean yes) {
2030        if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) {
2031            InstanceManager.getDefault(ProgrammerConfigManager.class).setShowCvNumbers(yes);
2032        }
2033    }
2034
2035    public static boolean getShowCvNumbers() {
2036        return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null ||
2037                InstanceManager.getDefault(ProgrammerConfigManager.class).isShowCvNumbers();
2038    }
2039
2040    public static void setCanCacheDefault(boolean yes) {
2041        if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) {
2042            InstanceManager.getDefault(ProgrammerConfigManager.class).setCanCacheDefault(yes);
2043        }
2044    }
2045
2046    public static boolean getCanCacheDefault() {
2047        return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null ||
2048                InstanceManager.getDefault(ProgrammerConfigManager.class).isCanCacheDefault();
2049    }
2050
2051    public static void setDoConfirmRead(boolean yes) {
2052        if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) {
2053            InstanceManager.getDefault(ProgrammerConfigManager.class).setDoConfirmRead(yes);
2054        }
2055    }
2056
2057    public static boolean getDoConfirmRead() {
2058        return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null ||
2059                InstanceManager.getDefault(ProgrammerConfigManager.class).isDoConfirmRead();
2060    }
2061
2062    public RosterEntry getRosterEntry() {
2063        return _rosterEntry;
2064    }
2065
2066    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(PaneProgFrame.class);
2067
2068}