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