001package jmri.jmrit.symbolicprog;
002
003import java.awt.BorderLayout;
004import java.awt.event.ActionListener;
005import java.beans.PropertyChangeEvent;
006import java.beans.PropertyChangeListener;
007import java.util.List;
008import java.util.ListIterator;
009import javax.annotation.*;
010import javax.swing.BoxLayout;
011import javax.swing.JButton;
012import javax.swing.JComboBox;
013import javax.swing.JLabel;
014import javax.swing.JPanel;
015import javax.swing.JToggleButton;
016import javax.swing.border.EmptyBorder;
017import jmri.GlobalProgrammerManager;
018import jmri.InstanceManager;
019import jmri.Programmer;
020import jmri.jmrit.decoderdefn.DecoderFile;
021import jmri.jmrit.decoderdefn.DecoderIndexFile;
022import jmri.jmrit.decoderdefn.IdentifyDecoder;
023import jmri.jmrit.progsupport.ProgModeSelector;
024import jmri.jmrit.roster.IdentifyLoco;
025import jmri.jmrit.roster.Roster;
026import jmri.jmrit.roster.RosterEntry;
027import jmri.jmrit.roster.RosterEntrySelector;
028import jmri.jmrit.roster.swing.GlobalRosterEntryComboBox;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032/**
033 * Provide GUI controls to select a known loco and/or new decoder.
034 * <p>
035 * When the "open programmer" button is pushed, i.e. the user is ready to
036 * continue, the startProgrammer method is invoked. This should be overridden
037 * (e.g. in a local anonymous class) to create the programmer frame you're
038 * interested in.
039 * <p>
040 * To override this class to use a different decoder-selection GUI, replace
041 * members:
042 * <ul>
043 * <li>layoutDecoderSelection
044 * <li>updateForDecoderTypeID
045 * <li>updateForDecoderMfgID
046 * <li>updateForDecoderNotID
047 * <li>resetDecoder
048 * <li>isDecoderSelected
049 * <li>selectedDecoderName
050 * </ul>
051 *
052 * @author Bob Jacobsen Copyright (C) 2001, 2002
053 */
054public class CombinedLocoSelPane extends LocoSelPane implements PropertyChangeListener {
055
056    /**
057     * Provide GUI controls to select a known loco and/or new decoder.
058     *
059     * @param s        Reference to a JLabel that should be updated with status
060     *                 information as identification happens.
061     *
062     * @param selector Reference to a
063     *                 {@link jmri.jmrit.progsupport.ProgModeSelector} panel
064     *                 that configures the programming mode.
065     */
066    public CombinedLocoSelPane(JLabel s, ProgModeSelector selector) {
067        _statusLabel = s;
068        this.selector = selector;
069        init();
070    }
071
072    ProgModeSelector selector;
073
074    /**
075     * Create the panel used to select the decoder.
076     *
077     * @return a JPanel for handling the decoder-selection GUI
078     */
079    protected JPanel layoutDecoderSelection() {
080        JPanel pane1a = new JPanel();
081        pane1a.setLayout(new BoxLayout(pane1a, BoxLayout.X_AXIS));
082        pane1a.add(new JLabel("Decoder installed: "));
083        decoderBox = InstanceManager.getDefault(DecoderIndexFile.class).matchingComboBox(null, null, null, null, null, null);
084        decoderBox.getAccessibleContext().setAccessibleName("Decoder installed: ");
085        decoderBox.insertItemAt("<from locomotive settings>", 0);
086        decoderBox.setSelectedIndex(0);
087        decoderBox.addActionListener(new ActionListener() {
088
089            @Override
090            public void actionPerformed(java.awt.event.ActionEvent e) {
091                if (decoderBox.getSelectedIndex() != 0) {
092                    // reset and disable loco selection
093                    locoBox.setSelectedIndex(0);
094                    go2.setEnabled(true);
095                    go2.setRequestFocusEnabled(true);
096                    go2.requestFocus();
097                    go2.setToolTipText(Bundle.getMessage("TipClickToOpen"));
098                } else {
099                    go2.setEnabled(false);
100                    go2.setToolTipText(Bundle.getMessage("TipSelectLoco"));
101                }
102            }
103        });
104        pane1a.add(decoderBox);
105        iddecoder = addDecoderIdentButton();
106        if (iddecoder != null) {
107            pane1a.add(iddecoder);
108        }
109        pane1a.setAlignmentX(JLabel.RIGHT_ALIGNMENT);
110        return pane1a;
111    }
112
113    /**
114     * Add a decoder identification button.
115     *
116     * @return the button
117     */
118    JToggleButton addDecoderIdentButton() {
119        JToggleButton button = new JToggleButton(Bundle.getMessage("ButtonReadType"));
120        button.setToolTipText(Bundle.getMessage("TipSelectType"));
121        button.getAccessibleContext().setAccessibleName(Bundle.getMessage("ButtonReadType"));
122        if (InstanceManager.getNullableDefault(GlobalProgrammerManager.class) != null) {
123            Programmer p = InstanceManager.getDefault(GlobalProgrammerManager.class).getGlobalProgrammer();
124            if (p != null && !p.getCanRead()) {
125                // can't read, disable the button
126                button.setEnabled(false);
127                button.setToolTipText(Bundle.getMessage("TipNoRead"));
128            }
129        }
130        button.addActionListener(new ActionListener() {
131            @Override
132            public void actionPerformed(java.awt.event.ActionEvent e) {
133                startIdentifyDecoder();
134            }
135        });
136        return button;
137    }
138
139    /**
140     * Set the decoder GUI back to having no selection.
141     *
142     * @param loco the loco name
143     */
144    void setDecoderSelectionFromLoco(String loco) {
145        decoderBox.setSelectedIndex(0);
146    }
147
148    /**
149     * Has the user selected a decoder type, either manually or via a successful
150     * event?
151     *
152     * @return true if a decoder type is selected
153     */
154    boolean isDecoderSelected() {
155        return decoderBox.getSelectedIndex() != 0;
156    }
157
158    /**
159     * Convert the decoder selection UI result into a name.
160     *
161     * @return The selected decoder type name, or null if none selected.
162     */
163    protected String selectedDecoderType() {
164        if (!isDecoderSelected()) {
165            return null;
166        } else {
167            return (String) decoderBox.getSelectedItem();
168        }
169    }
170
171    /**
172     * Create the panel used to select an existing entry.
173     *
174     * @return a JPanel for handling the entry-selection GUI
175     */
176    protected JPanel layoutRosterSelection() {
177        JPanel pane2a = new JPanel();
178        pane2a.setLayout(new BoxLayout(pane2a, BoxLayout.X_AXIS));
179        pane2a.add(new JLabel(Bundle.getMessage("USE LOCOMOTIVE SETTINGS FOR:")));
180        locoBox.getAccessibleContext().setAccessibleName(Bundle.getMessage("USE LOCOMOTIVE SETTINGS FOR:"));
181        locoBox.setNonSelectedItem(Bundle.getMessage("<NONE - NEW LOCO>"));
182        Roster.getDefault().addPropertyChangeListener(this);
183        pane2a.add(locoBox);
184        locoBox.addPropertyChangeListener(RosterEntrySelector.SELECTED_ROSTER_ENTRIES, new PropertyChangeListener() {
185
186            @Override
187            public void propertyChange(PropertyChangeEvent pce) {
188                if (locoBox.getSelectedRosterEntries().length != 0) {
189                    // reset and disable decoder selection
190                    setDecoderSelectionFromLoco(locoBox.getSelectedRosterEntries()[0].titleString());
191                    go2.setEnabled(true);
192                    go2.setRequestFocusEnabled(true);
193                    go2.requestFocus();
194                    go2.setToolTipText(Bundle.getMessage("TipClickToOpen"));
195                } else {
196                    go2.setEnabled(false);
197                    go2.setToolTipText(Bundle.getMessage("TipSelectLoco"));
198                }
199            }
200        });
201        idloco = new JToggleButton(Bundle.getMessage("IDENT"));
202        idloco.getAccessibleContext().setAccessibleName(Bundle.getMessage("IDENT"));
203        idloco.setToolTipText(Bundle.getMessage("READ THE LOCOMOTIVE'S ADDRESS AND ATTEMPT TO SELECT THE RIGHT SETTINGS"));
204        if (InstanceManager.getNullableDefault(GlobalProgrammerManager.class) != null) {
205            Programmer p = InstanceManager.getDefault(GlobalProgrammerManager.class).getGlobalProgrammer();
206            if (p != null && !p.getCanRead()) {
207                // can't read, disable the button
208                idloco.setEnabled(false);
209                idloco.setToolTipText(Bundle.getMessage("BUTTON DISABLED BECAUSE CONFIGURED COMMAND STATION CAN'T READ CVS"));
210            }
211        }
212        idloco.addActionListener(new ActionListener() {
213            @Override
214            public void actionPerformed(java.awt.event.ActionEvent e) {
215                if (log.isDebugEnabled()) {
216                    log.debug("Identify locomotive pressed");
217                }
218                startIdentifyLoco();
219            }
220        });
221        pane2a.add(idloco);
222        pane2a.setAlignmentX(JLabel.RIGHT_ALIGNMENT);
223        return pane2a;
224    }
225
226    /**
227     * Initialize the GUI.
228     */
229    protected void init() {
230        //setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
231        setLayout(new BorderLayout());
232
233        JPanel pane2a = layoutRosterSelection();
234        if (pane2a != null) {
235            add(pane2a, BorderLayout.NORTH);
236        }
237
238        add(layoutDecoderSelection(), BorderLayout.CENTER);
239
240        add(createProgrammerSelection(), BorderLayout.SOUTH);
241        setBorder(new EmptyBorder(6, 6, 6, 6));
242    }
243
244    /**
245     * Creates a Programmer Selection panel.
246     *
247     * @return the panel
248     */
249    protected JPanel createProgrammerSelection() {
250        JPanel pane3a = new JPanel();
251        pane3a.setLayout(new BoxLayout(pane3a, BoxLayout.Y_AXIS));
252        // create the programmer box
253        JPanel progFormat = new JPanel();
254        progFormat.setLayout(new BoxLayout(progFormat, BoxLayout.X_AXIS));
255        progFormat.add(new JLabel(Bundle.getMessage("ProgrammerFormat")));
256        progFormat.setAlignmentX(JLabel.RIGHT_ALIGNMENT);
257
258        programmerBox = new JComboBox<>(ProgDefault.findListOfProgFiles());
259        programmerBox.setSelectedIndex(0);
260        if (ProgDefault.getDefaultProgFile() != null) {
261            programmerBox.setSelectedItem(ProgDefault.getDefaultProgFile());
262        }
263        progFormat.add(programmerBox);
264
265        go2 = new JButton(Bundle.getMessage("OpenProgrammer"));
266        go2.getAccessibleContext().setAccessibleName(Bundle.getMessage("OpenProgrammer"));
267        go2.addActionListener(new ActionListener() {
268            @Override
269            public void actionPerformed(java.awt.event.ActionEvent e) {
270                if (log.isDebugEnabled()) {
271                    log.debug("Open programmer pressed");
272                }
273                openButton();
274            }
275        });
276        go2.setAlignmentX(JLabel.RIGHT_ALIGNMENT);
277        go2.setEnabled(false);
278        go2.setToolTipText(Bundle.getMessage("TipSelectLoco"));
279        pane3a.add(progFormat);
280        pane3a.add(go2);
281        return pane3a;
282    }
283
284    /**
285     * Reference to an external (not in this pane) JLabel that should be updated
286     * with status information as identification happens.
287     */
288    JLabel _statusLabel = null;
289
290    /**
291     * Identify loco button pressed, start the identify operation This defines
292     * what happens when the identify is done.
293     */
294    protected void startIdentifyLoco() {
295        // start identifying a loco
296        Programmer p = null;
297        if (selector != null && selector.isSelected()) {
298            p = selector.getProgrammer();
299        }
300        if (p == null) {
301            log.warn("Selector did not provide a programmer, use default");
302            p = InstanceManager.getDefault(GlobalProgrammerManager.class).getGlobalProgrammer();
303        }
304        IdentifyLoco id = new IdentifyLoco(p) {
305
306            @Override
307            protected void done(int dccAddress) {
308                // if Done, updated the selected decoder
309                CombinedLocoSelPane.this.selectLoco(dccAddress);
310            }
311
312            @Override
313            protected void message(String m) {
314                if (_statusLabel != null) {
315                    _statusLabel.setText(m);
316                }
317            }
318
319            @Override
320            protected void error() {
321                // raise the button again
322                idloco.setSelected(false);
323            }
324        };
325        id.start();
326    }
327
328    /**
329     * Identify loco button pressed, start the identify operation. This defines
330     * what happens when the identify is done.
331     */
332    protected void startIdentifyDecoder() {
333        // start identifying a decoder
334        Programmer p = null;
335        if (selector != null && selector.isSelected()) {
336            p = selector.getProgrammer();
337        }
338        if (p == null) {
339            log.warn("Selector did not provide a programmer, use default");
340            p = InstanceManager.getDefault(GlobalProgrammerManager.class).getGlobalProgrammer();
341        }
342        IdentifyDecoder id = new IdentifyDecoder(p) {
343
344            @Override
345            protected void done(int mfg, int model, int productID) {
346                // if Done, updated the selected decoder
347                CombinedLocoSelPane.this.selectDecoder(mfg, model, productID);
348            }
349
350            @Override
351            protected void message(String m) {
352                if (_statusLabel != null) {
353                    _statusLabel.setText(m);
354                }
355            }
356
357            @Override
358            protected void error() {
359                // raise the button again
360                iddecoder.setSelected(false);
361            }
362        };
363        id.start();
364    }
365
366    /**
367     * Notification that the Roster has changed, so the locomotive selection
368     * list has to be changed.
369     *
370     * @param ev Ignored.
371     */
372    @Override
373    public void propertyChange(PropertyChangeEvent ev) {
374        locoBox.update();
375    }
376
377    /**
378     * Identify locomotive complete, act on it by setting the GUI. This will
379     * fire "GUI changed" events which will reset the decoder GUI.
380     *
381     * @param dccAddress the address to select
382     */
383    protected void selectLoco(int dccAddress) {
384        // raise the button again
385        idloco.setSelected(false);
386        // locate that loco
387        List<RosterEntry> l = Roster.getDefault().matchingList(null, null, Integer.toString(dccAddress),
388                null, null, null, null);
389        if (log.isDebugEnabled()) {
390            log.debug("selectLoco found {} matches", l.size());
391        }
392        if (l.size() > 0) {
393            RosterEntry r = l.get(0);
394            if (log.isDebugEnabled()) {
395                log.debug("Loco id is {}", r.getId());
396            }
397            locoBox.setSelectedItem(r);
398        } else {
399            log.warn("Read address {}, but no such loco in roster", dccAddress);
400            _statusLabel.setText(Bundle.getMessage("ReadNoSuchLoco",dccAddress));
401        }
402    }
403
404    /**
405     * Identify decoder complete, act on it by setting the GUI This will fire
406     * "GUI changed" events which will reset the locomotive GUI.
407     *
408     * @param mfgID     the decoder's manufacturer ID value from CV8
409     * @param modelID   the decoder's model ID value from CV7
410     * @param productID the decoder's product ID
411     */
412    protected void selectDecoder(int mfgID, int modelID, int productID) {
413        // raise the button again
414        iddecoder.setSelected(false);
415        List<DecoderFile> temp = null;
416
417        // if productID present, try with that
418        if (productID != -1) {
419            String sz_productID = Integer.toString(productID);
420            temp = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, null, Integer.toString(mfgID), Integer.toString(modelID), sz_productID, null);
421            if (temp.isEmpty()) {
422                log.debug("selectDecoder found no items with product ID {}", productID);
423                temp = null;
424            } else {
425                log.debug("selectDecoder found {} matches with productID {}", temp.size(), productID);
426            }
427        }
428
429        // try without product ID if needed
430        if (temp == null) {  // i.e. if no match previously
431            temp = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, null, Integer.toString(mfgID), Integer.toString(modelID), null, null);
432            if (log.isDebugEnabled()) {
433                log.debug("selectDecoder without productID found {} matches", temp.size());
434            }
435        }
436
437        // remove unwanted matches
438        int tempOriginalSize = temp.size(); // save size of unfiltered list
439        String theFamily = "";
440        String theModel = "";
441        String lastWasFamily = "";
442
443        ListIterator<DecoderFile> it = temp.listIterator();
444        while (it.hasNext()) {
445            log.debug("Match List size is currently {}, scanning for unwanted entries", temp.size());
446            DecoderFile t = it.next();
447            theFamily = t.getFamily();
448            theModel = t.getModel();
449            if (t.getFamily().equals(theModel)) {
450                log.debug("Match List index={} is family entry '{}'", it.previousIndex(), theFamily);
451                lastWasFamily = theFamily;
452            } else if (lastWasFamily.equals(theFamily)) {
453                log.debug("Match List index={} is first model '{}' in family '{}'", it.previousIndex(), theModel, theFamily);
454                log.debug("Removing family entry '{}'", theFamily);
455                t = it.previous();
456                t = it.previous();
457                it.remove();
458                lastWasFamily = "";
459            } else if ((t.getModelElement().getAttribute("show") != null)
460                    && (t.getModelElement().getAttribute("show").getValue().equals("no"))) {
461                log.debug("Match List index={} is legacy model '{}' in family '{}'", it.previousIndex(), theModel, theFamily);
462                log.debug("Removing legacy model '{}'", theModel);
463                t = it.previous();
464                it.remove();
465                lastWasFamily = "";
466            } else {
467                log.debug("Match List index={} is model '{}' in family '{}'", it.previousIndex(), theModel, theFamily);
468                lastWasFamily = "";
469            }
470        }
471
472        log.debug("Final Match List size is {}", temp.size());
473
474        // If we had match(es) previously but have lost them in filtering
475        // pretend we have no product ID so we get a coarse match
476        if (tempOriginalSize > 0 && temp.isEmpty()) {
477            log.debug("Filtering removed all matches so reverting to coarse match with mfgID='{}' & modelID='{}'",
478                    Integer.toString(mfgID), Integer.toString(modelID));
479            temp = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, null, Integer.toString(mfgID), Integer.toString(modelID), null, null);
480            log.debug("selectDecoder without productID found {} matches", temp.size());
481        }
482
483        // install all those in the JComboBox in place of the longer, original list
484        if (temp.size() > 0) {
485            updateForDecoderTypeID(temp);
486        } else {
487            String mfg = InstanceManager.getDefault(DecoderIndexFile.class).mfgNameFromID(Integer.toString(mfgID));
488            if (mfg == null) {
489                updateForDecoderNotID(mfgID, modelID);
490            } else {
491                updateForDecoderMfgID(mfg, mfgID, modelID);
492            }
493        }
494    }
495
496    /**
497     * Decoder identify has matched one or more specific types.
498     *
499     * @param pList a list of decoders
500     */
501    void updateForDecoderTypeID(List<DecoderFile> pList) {
502        decoderBox.setModel(DecoderIndexFile.jComboBoxModelFromList(pList));
503        decoderBox.insertItemAt("<from locomotive settings>", 0);
504        decoderBox.setSelectedIndex(1);
505    }
506
507    /**
508     * Decoder identify has not matched specific types, but did find
509     * manufacturer match.
510     *
511     * @param pMfg     Manufacturer name. This is passed to save time, as it has
512     *                 already been determined once.
513     * @param pMfgID   Manufacturer ID number (CV8)
514     * @param pModelID Model ID number (CV7)
515     */
516    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
517        justification="String also built for display in _statusLabel")
518    void updateForDecoderMfgID(String pMfg, int pMfgID, int pModelID) {
519        String msg = "Found mfg " + pMfgID + " (" + pMfg + ") version " + pModelID + "; no such decoder defined";
520        log.warn(msg);
521        _statusLabel.setText(msg);
522        // try to select all decoders from that MFG
523        JComboBox<String> temp = InstanceManager.getDefault(DecoderIndexFile.class).matchingComboBox(null, null, Integer.toString(pMfgID), null, null, null);
524        if (log.isDebugEnabled()) {
525            log.debug("mfg-only selectDecoder found {} matches", temp.getItemCount());
526        }
527        // install all those in the JComboBox in place of the longer, original list
528        if (temp.getItemCount() > 0) {
529            decoderBox.setModel(temp.getModel());
530            decoderBox.insertItemAt("<from locomotive settings>", 0);
531            decoderBox.setSelectedIndex(1);
532        } else {
533            // if there are none from this mfg, go back to showing everything
534            temp = InstanceManager.getDefault(DecoderIndexFile.class).matchingComboBox(null, null, null, null, null, null);
535            decoderBox.setModel(temp.getModel());
536            decoderBox.insertItemAt("<from locomotive settings>", 0);
537            decoderBox.setSelectedIndex(1);
538        }
539    }
540
541    /**
542     * Decoder identify did not match anything, warn and show all.
543     *
544     * @param pMfgID   Manufacturer ID number (CV8)
545     * @param pModelID Model ID number (CV7)
546     */
547    void updateForDecoderNotID(int pMfgID, int pModelID) {
548        log.warn("Found mfg {} version {}; no such manufacturer defined", pMfgID, pModelID);
549        JComboBox<String> temp = InstanceManager.getDefault(DecoderIndexFile.class).matchingComboBox(null, null, null, null, null, null);
550        decoderBox.setModel(temp.getModel());
551        decoderBox.insertItemAt("<from locomotive settings>", 0);
552        decoderBox.setSelectedIndex(1);
553    }
554
555    protected GlobalRosterEntryComboBox locoBox = new GlobalRosterEntryComboBox();
556    private JComboBox<String> decoderBox = null;       // private because children will override this
557    protected JComboBox<String> programmerBox = null;
558    protected JToggleButton iddecoder;
559    protected JToggleButton idloco;
560    protected JButton go2;
561
562    /**
563     * handle pushing the open programmer button by finding names, then calling
564     * a template method.
565     */
566    protected void openButton() {
567        // figure out which we're dealing with
568        if (locoBox.getSelectedRosterEntries().length != 0) {
569            // known loco
570            openKnownLoco();
571        } else if (isDecoderSelected()) {
572            // new loco
573            openNewLoco();
574        } else {
575            // should not happen, as the button should be disabled!
576            log.error("openButton with neither combobox nonzero");
577        }
578    }
579
580    /**
581     * Start with a locomotive selected, so we're opening an existing
582     * RosterEntry.
583     */
584    protected void openKnownLoco() {
585
586        if (locoBox.getSelectedRosterEntries().length != 0) {
587            RosterEntry re = locoBox.getSelectedRosterEntries()[0];
588            if (log.isDebugEnabled()) {
589                log.debug("loco file: {}", re.getFileName());
590            }
591
592            startProgrammer(null, re, (String) programmerBox.getSelectedItem());
593        } else {
594            log.error("No roster entry was selected to open.");
595        }
596    }
597
598    /**
599     * Start with a decoder selected, so we're going to create a new
600     * RosterEntry.
601     */
602    protected void openNewLoco() {
603        // find the decoderFile object
604        DecoderFile decoderFile = InstanceManager.getDefault(DecoderIndexFile.class).fileFromTitle(selectedDecoderType());
605        if (log.isDebugEnabled()) {
606            log.debug("decoder file: {}", decoderFile.getFileName());
607        }
608
609        // create a dummy RosterEntry with the decoder info
610        RosterEntry re = new RosterEntry();
611        re.setDecoderFamily(decoderFile.getFamily());
612        re.setDecoderModel(decoderFile.getModel());
613        re.setId(Bundle.getMessage("LabelNewDecoder"));
614        // note that we're leaving the filename null
615        // add the new roster entry to the in-memory roster
616        Roster.getDefault().addEntry(re);
617
618        startProgrammer(decoderFile, re, (String) programmerBox.getSelectedItem());
619    }
620
621    /**
622     * Start the desired type of programmer.
623     *
624     * @param decoderFile defines the type of decoder installed; if null, check
625     *                    the RosterEntry re for that
626     * @param r           Existing roster entry defining this locomotive
627     * @param progName    name of the programmer (Layout connection) being used
628     */
629    // TODO: Fix inheritance.  This is both a base class (where startProgrammer really isn't part of the contract_
630    //       and a first implementation (where this method is needed).  Because it's part of the contract, it can't be
631    //       made abstract:  CombinedLocoSelListPane and CombinedLocoSelTreePane have no need for it.
632    protected void startProgrammer(@CheckForNull DecoderFile decoderFile, @Nonnull RosterEntry r, @Nonnull String progName) {
633        log.error("startProgrammer method in CombinedLocoSelPane should have been overridden");
634    }
635
636    private final static Logger log = LoggerFactory.getLogger(CombinedLocoSelPane.class);
637
638}