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}