001package jmri.jmrit.roster; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004import java.awt.BorderLayout; 005import java.awt.Component; 006import java.awt.Container; 007import java.awt.Frame; 008import java.awt.event.ActionEvent; 009import java.awt.event.FocusEvent; 010import java.awt.event.FocusListener; 011import java.awt.event.KeyEvent; 012import java.awt.event.WindowAdapter; 013import java.awt.event.WindowEvent; 014import java.io.IOException; 015import java.util.ArrayList; 016import java.util.Arrays; 017import java.util.List; 018import javax.swing.BoxLayout; 019import javax.swing.Icon; 020import javax.swing.JButton; 021import javax.swing.JComponent; 022import javax.swing.JDialog; 023import javax.swing.JFrame; 024import javax.swing.JLabel; 025import javax.swing.JOptionPane; 026import javax.swing.JPanel; 027import javax.swing.JScrollPane; 028import javax.swing.JSeparator; 029import javax.swing.JToggleButton; 030import javax.swing.KeyStroke; 031import javax.swing.SwingConstants; 032import javax.swing.event.TreeSelectionEvent; 033import javax.swing.tree.TreeNode; 034import jmri.InstanceManager; 035import jmri.jmrit.decoderdefn.DecoderFile; 036import jmri.jmrit.decoderdefn.DecoderIndexFile; 037import jmri.jmrit.symbolicprog.CombinedLocoSelTreePane; 038import jmri.jmrit.symbolicprog.CvTableModel; 039import jmri.jmrit.symbolicprog.CvValue; 040import jmri.jmrit.symbolicprog.SymbolicProgBundle; 041import jmri.util.swing.JmriAbstractAction; 042import jmri.util.swing.WindowInterface; 043import org.slf4j.Logger; 044import org.slf4j.LoggerFactory; 045 046/** 047 * Update the decoder definitions in the roster. 048 * <br><br> 049 * When required, provides a user GUI to assist with replacing multiple-match 050 * definitions. 051 * 052 * @author Bob Jacobsen Copyright (C) 2013 053 * @see jmri.jmrit.XmlFile 054 * @author Dave Heap 2017 - Provide user GUI 055 */ 056public class UpdateDecoderDefinitionAction extends JmriAbstractAction { 057 058 /** 059 * The prefix string used to specify a query in decoder definition file 060 * replacementFamily and replacementModel elements. 061 */ 062 public static final String QRY_PREFIX = "query:"; 063 064 /** 065 * The {@link java.util.regex regex} separator to 066 * {@link java.lang.String#split(java.lang.String) split} items in 067 * replacementFamily and replacementModel elements. 068 */ 069 public static final String QRY_SEPARATOR = "\\|"; 070 071 /** 072 * The {@code replacementFamily} attribute from the decoder definition file. 073 */ 074 String replacementFamily; 075 String replacementFamilyString; // replacementFamily with any QRY_PREFIX stripped 076 boolean hasReplacementFamilyQuery; // whether replacementFamily has a QRY_PREFIX 077 078 /** 079 * The {@code replacementModel} attribute from the decoder definition file. 080 */ 081 String replacementModel; 082 String replacementModelString; // replacementModel with any QRY_PREFIX stripped 083 boolean hasReplacementModelQuery; // whether replacementModel has a QRY_PREFIX 084 int cV7Value; // the CV7 (versionID) value stored in the roster entry 085 int cV8Value; // the CV8 (mfgID) value stored in the roster entry 086 087 /** 088 * Displays the last-selected filter action. 089 */ 090 JLabel lastActionDisplay; 091 092 /** 093 * A temporary roster entry used in matching and replacement. 094 */ 095 transient volatile RosterEntry tempRe; 096 097 /** 098 * A {@link List} based on the combination of any 099 * replacementFamily and 100 * replacementModel suggestions. 101 */ 102 transient volatile List<DecoderFile> replacementList; 103 104 /** 105 * The subset of the <code>replacementList</code> that also matches 106 * both the 107 * {@link jmri.jmrit.decoderdefn.IdentifyDecoder} manufacturerID 108 * stored in CV8 and the 109 * {@link jmri.jmrit.decoderdefn.IdentifyDecoder} versionID stored 110 * in CV7. 111 */ 112 transient volatile List<DecoderFile> versionMatchList; 113 114 transient volatile DecoderIndexFile di; // the default instance of the DecoderIndexFile 115 transient volatile FocusListener fListener; 116 transient volatile JLabel statusLabel; 117 transient volatile JDialog f; 118 119 final jmri.jmrit.progsupport.ProgModeSelector modePane = new jmri.jmrit.progsupport.ProgServiceModeComboBox(); 120 JButton cancelButton; 121 JToggleButton versionButton; 122 JToggleButton replacementButton; 123 CombinedLocoSelTreePane combinedLocoSelTree; 124 125 /** 126 * Update the decoder definitions in the roster. 127 * 128 * @param name the name ({@link javax.swing.Action#NAME}) for the action; a 129 * value of {@code null} is ignored 130 */ 131 public UpdateDecoderDefinitionAction(String name) { 132 super(name); 133 } 134 135 /** 136 * Update the decoder definitions in the roster. 137 * 138 * @param name the name ({@link javax.swing.Action#NAME}) for the action; a 139 * value of {@code null} is ignored 140 * @param wi the window interface controlling how this action is displayed 141 */ 142 public UpdateDecoderDefinitionAction(String name, WindowInterface wi) { 143 super(name, wi); 144 } 145 146 /** 147 * Update the decoder definitions in the roster. 148 * 149 * @param name the name ({@link javax.swing.Action#NAME}) for the action; a 150 * value of {@code null} is ignored 151 * @param i the small icon ({@link javax.swing.Action#SMALL_ICON}) for 152 * the action; a value of {@code null} is ignored 153 * @param wi the window interface controlling how this action is displayed 154 */ 155 public UpdateDecoderDefinitionAction(String name, Icon i, WindowInterface wi) { 156 super(name, i, wi); 157 } 158 159 @Override 160 public synchronized void actionPerformed(ActionEvent e) { 161 List<RosterEntry> list = Roster.getDefault().matchingList(null, null, null, null, null, null, null); 162 163 boolean skipQueries = false; 164 165 di = InstanceManager.getDefault(DecoderIndexFile.class); 166 167 for (RosterEntry entry : list) { 168 String family = entry.getDecoderFamily(); 169 String model = entry.getDecoderModel(); 170 171 // check if replaced or missing 172 List<DecoderFile> decoders = di.matchingDecoderList(null, family, null, null, null, model); 173 boolean missing = decoders.size() < 1; 174 if (decoders.size() != 1 && !model.equals(family)) { 175 log.error("Found {} decoders matching family \"{}\" model \"{}\" from roster entry \"{}\"", 176 decoders.size(), family, model, entry.getId()); 177 if (missing) { 178 replacementModel = model; // fall back to try just the decoder name, not family 179 replacementModelString = replacementModel; 180 replacementFamily = null; 181 replacementFamilyString = ""; 182 } else { 183 continue; // cannot process this one; there are multiple definitions for the same family/model combination 184 } 185 } 186 187 for (DecoderFile decoder : decoders) { 188 if (decoder.getReplacementFamily() != null || decoder.getReplacementModel() != null) { 189 log.debug("Indicated replacements are family \"{}\" model \"{}\"", 190 decoder.getReplacementFamily(), decoder.getReplacementModel()); 191 } 192 replacementFamily = decoder.getReplacementFamily(); 193 replacementModel = decoder.getReplacementModel(); 194 hasReplacementFamilyQuery = false; 195 hasReplacementModelQuery = false; 196 replacementFamilyString = replacementFamily; 197 replacementModelString = replacementModel; 198 if (replacementFamily != null && replacementFamily.startsWith(QRY_PREFIX)) { 199 hasReplacementFamilyQuery = true; 200 replacementFamilyString = replacementFamily.substring(QRY_PREFIX.length()); 201 } else if (replacementFamily == null) { 202 replacementFamilyString = family; 203 } 204 if (replacementModel != null && replacementModel.startsWith(QRY_PREFIX)) { 205 hasReplacementModelQuery = true; 206 replacementModelString = replacementModel.substring(QRY_PREFIX.length()); 207 } else if (replacementModel == null) { 208 replacementModelString = model; 209 } 210 log.trace("String replacements are family \"{}\", query={} and model \"{}\", query={}", 211 replacementFamilyString, hasReplacementFamilyQuery, replacementModelString, hasReplacementModelQuery); 212 } 213 214 if (replacementModel != null || replacementFamily != null) { 215 216 boolean isToUpdate = true; 217 if ((replacementModel != null && replacementModel.startsWith(QRY_PREFIX)) 218 || (replacementFamily != null && replacementFamily.startsWith(QRY_PREFIX)) 219 || missing) { 220 int retVal = 2; 221 if (!skipQueries) { 222 // build explanatory text 223 StringBuilder sb = new StringBuilder(); 224 sb.append(Bundle.getMessage("TextMultRepl1", entry.getId(), family, model)).append("\n\n") 225 .append(Bundle.getMessage(missing ? "TextNoDefn1a" : "TextMultRepl1a")).append("\n"); 226 227 if (replacementFamily != null && !replacementFamily.equals(family) && !replacementFamily.equals(QRY_PREFIX)) { 228 if (replacementFamily.startsWith(QRY_PREFIX)) { 229 sb.append(Bundle.getMessage("TextMultReplFamilyOneOf")).append(": \""); 230 sb.append(replacementFamily.substring(QRY_PREFIX.length()).replaceAll(QRY_SEPARATOR, "\",\"")); 231 sb.append("\"\n"); 232 } else { 233 sb.append(Bundle.getMessage("TextMultReplFamily")); 234 sb.append(": \"").append(replacementFamily).append("\"\n"); 235 } 236 } 237 if (replacementModel != null && !replacementModel.equals(model) && !replacementModel.equals(QRY_PREFIX)) { 238 if (replacementModel.startsWith(QRY_PREFIX)) { 239 sb.append(Bundle.getMessage("TextMultReplModelOneOf")).append(": \""); 240 sb.append(replacementModel.substring(QRY_PREFIX.length()).replaceAll(QRY_SEPARATOR, "\",\"")); 241 sb.append("\"\n"); 242 } else { 243 sb.append(Bundle.getMessage("TextMultReplModel")); 244 sb.append(": \"").append(replacementModel).append("\"\n"); 245 } 246 } 247 248 sb.append("\n").append(Bundle.getMessage("TextMultRepl2", Bundle.getMessage("ButtonMultReplSelectNew"))); 249 sb.append("\n"); 250 251 retVal = multiReplacementDialog(sb.toString(), missing); 252 } 253 log.trace("return value = {}", retVal); 254 if (retVal == 2) { 255 skipQueries = true; 256 log.trace("Skip All"); 257 } 258 if (retVal != 0) { 259 log.trace("Skip This"); 260 isToUpdate = false; 261 } 262 log.trace("Is to Update = {}", isToUpdate); 263 if (isToUpdate) { 264 decoderSelectionPane(entry); 265 if (tempRe == null) { 266 log.trace("dummy Roster Entry is null"); 267 isToUpdate = false; 268 } else { 269 log.trace("dummy Roster Entry returned Family '{}', model '{}'", tempRe.getDecoderFamily(), tempRe.getDecoderModel()); 270 if (!tempRe.getDecoderFamily().equals(family)) { 271 replacementFamily = tempRe.getDecoderFamily(); 272 } else { 273 replacementFamily = null; 274 } 275 if (!tempRe.getDecoderModel().equals(model)) { 276 replacementModel = tempRe.getDecoderModel(); 277 } else { 278 replacementModel = null; 279 } 280 } 281 } 282 } 283 284 // change the roster entry 285 if (isToUpdate) { 286 if (replacementFamily != null) { 287 log.info(" *** Will update \"{}'\". replacementFamily='{}'", entry.getId(), replacementFamily); 288 entry.setDecoderFamily(replacementFamily); 289 } 290 if (replacementModel != null) { 291 log.info(" *** Will update \"{}'\". replacementModel='{}'", entry.getId(), replacementModel); 292 entry.setDecoderModel(replacementModel); 293 } 294 295 // write it out (not bothering to do backup?) 296 entry.updateFile(); 297 } 298 } 299 } 300 301 // write updated roster 302 Roster.getDefault() 303 .makeBackupFile(Roster.getDefault().getRosterIndexPath()); 304 try { 305 Roster.getDefault().writeFile(Roster.getDefault().getRosterIndexPath()); 306 } catch (IOException ex) { 307 log.error("Exception while writing the new roster file, may not be complete", ex); 308 } 309 // use the new one 310 311 Roster.getDefault() 312 .reloadRosterFile(); 313 } 314 315 /** 316 * Fetch the {@link JOptionPane} associated with this {@link JComponent}. 317 * <br><br> 318 * Note that: 319 * <ul> 320 * <li>The {@code source} must be within (or itself be) a 321 * {@link JOptionPane}.</li> 322 * <li>If {@code source} is a {@link JOptionPane}, the returned element will 323 * be {@code source}</li> 324 * </ul> 325 * 326 * @param source the {@link JComponent} 327 * @return the {@link JOptionPane} associated with {@code source} 328 */ 329 @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", 330 justification = "Code calls method in such a way that the cast is guaranteed to be safe") // NOI18N 331 synchronized JOptionPane getOptionPane(JComponent source) { 332 JOptionPane pane; 333 if (!(source instanceof JOptionPane)) { 334 pane = getOptionPane((JComponent) source.getParent()); 335 } else { 336 pane = (JOptionPane) source; 337 } 338 return pane; 339 } 340 341 /** 342 * Creates the "Multiple Replacements Found" dialog box with custom buttons 343 * and tooltips. 344 * 345 * @param text the explanatory text to display 346 * @param missing if true, displays a "missing definition" title rather than 347 * a "multiple replacements" title 348 * @return sequence number of the button selected 349 */ 350 synchronized int multiReplacementDialog(String text, boolean missing) { 351 // Create custom buttons so we can add tooltips 352 final JButton select = new JButton(Bundle.getMessage("ButtonMultReplSelectNew")); 353 select.setToolTipText(Bundle.getMessage("ToolTipMultReplSelectNew")); 354 select.addActionListener((ActionEvent e) -> { 355 JOptionPane pane = getOptionPane((JComponent) e.getSource()); 356 pane.setValue(select); 357 }); 358 final JButton skipThis = new JButton(Bundle.getMessage("ButtonMultReplSkipThis")); 359 skipThis.setToolTipText(Bundle.getMessage("ToolTipMultReplSkipThis")); 360 skipThis.addActionListener((ActionEvent e) -> { 361 JOptionPane pane = getOptionPane((JComponent) e.getSource()); 362 pane.setValue(skipThis); 363 }); 364 final JButton skipAll = new JButton(Bundle.getMessage("ButtonMultReplSkipAll")); 365 skipAll.setToolTipText(Bundle.getMessage("ToolTipMultReplSkipAll")); 366 skipAll.addActionListener((ActionEvent e) -> { 367 JOptionPane pane = getOptionPane((JComponent) e.getSource()); 368 pane.setValue(skipAll); 369 }); 370 int retVal = JOptionPane.CLOSED_OPTION; 371 372 while (retVal == JOptionPane.CLOSED_OPTION) { 373 retVal = JOptionPane.showOptionDialog(new JFrame(), 374 text, 375 Bundle.getMessage(missing ? "TitleNoDefn" : "TitleMultRepl"), 376 JOptionPane.DEFAULT_OPTION, 377 JOptionPane.PLAIN_MESSAGE, 378 null, 379 new JButton[]{select, skipThis, skipAll}, 380 select); 381 log.trace("retVal={}", retVal); 382 } 383 return retVal; 384 } 385 386 /** 387 * Creates the "Replacement Definition" pane, which is similar in appearance 388 * to {@link apps.gui3.dp3.PaneProgDp3Action Create New Loco} pane, likewise 389 * utilizing a customized instance of {@link CombinedLocoSelTreePane}. 390 * 391 * @param theEntry an existing roster entry that needs replacement 392 */ 393 synchronized void decoderSelectionPane(RosterEntry theEntry) { 394 395 log.debug("Decoder Selection Pane requested"); // NOI18N 396 397 tempRe = null; 398 statusLabel = new JLabel(SymbolicProgBundle.getMessage("StateIdle")); // NOI18N 399 log.debug("New decoder requested"); // NOI18N 400 makeMatchLists(theEntry); 401 log.trace("Version matchlist size={}", versionMatchList.size()); 402 log.trace("Replacement matchlist size={}", replacementList.size()); 403 404 // based on code borrowed from apps.gui3.dp3.PaneProgDp3Action#actionPerformed 405 // create the initial frame that steers 406 f = new JDialog((Frame) null, Bundle.getMessage("TitleReplDefn", theEntry.getId()), true); // NOI18N 407 Container dialogPane = f.getContentPane(); 408 dialogPane.setLayout(new BoxLayout(dialogPane, BoxLayout.Y_AXIS)); 409 // ensure status line is cleared on close so it is normal if tempRe-opened 410 f.addWindowListener(new WindowAdapter() { 411 @Override 412 public synchronized void windowClosing(WindowEvent we) { 413 statusLabel.setText(SymbolicProgBundle.getMessage("StateIdle")); // NOI18N 414 log.debug("window closing"); 415 f.dispose(); 416 } 417 }); 418 f.getRootPane().registerKeyboardAction(e -> { 419 statusLabel.setText(SymbolicProgBundle.getMessage("StateIdle")); // NOI18N 420 log.debug("escape pressed"); 421 f.dispose(); 422 }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); 423 424 final JPanel bottomPanel = new JPanel(new BorderLayout()); 425 // new Loco on programming track 426 combinedLocoSelTree = new CombinedLocoSelTreePane(statusLabel, modePane) { 427 428 @Override 429 protected synchronized void openNewLoco() { 430 // find the decoderFile object 431 go2.setToolTipText(Bundle.getMessage("ToolTipUseSelectedDecoder")); 432 DecoderFile decoderFile = di.fileFromTitle(selectedDecoderType()); 433 log.debug("decoder file: {}", decoderFile.getFileName()); // NOI18N 434 // create a dummy RosterEntry with the decoder info 435 tempRe = new RosterEntry(); 436 tempRe.setDecoderFamily(decoderFile.getFamily()); 437 tempRe.setDecoderModel(decoderFile.getModel()); 438 tempRe.setId(SymbolicProgBundle.getMessage("LabelNewDecoder")); // NOI18N 439 // That's all, folks. The family and model will be picked up from tempRe in the main code. 440 } 441 442 @Override 443 protected synchronized JPanel layoutRosterSelection() { 444 log.debug("layoutRosterSelection"); 445 return null; 446 } 447 448 @Override 449 protected synchronized JPanel layoutDecoderSelection() { 450 log.debug("layoutDecoderSelection"); 451 JPanel pan = super.layoutDecoderSelection(); 452 versionButton = versionMatchButton(); 453 viewButtons.add(versionButton); 454 replacementButton = replacementMatchButton(); 455 viewButtons.add(replacementButton); 456 updateMatchButtons(theEntry); 457 dTree.removeTreeSelectionListener(dListener); 458 dListener = (TreeSelectionEvent e) -> { 459 log.debug("selection changed, {}empty, {}", 460 (dTree.isSelectionEmpty() ? "" : "not "), Arrays.toString(dTree.getSelectionPaths())); 461 if (dTree.hasFocus()) { 462 setLastActionDisplay("TextManualSelection"); 463 } 464 if (!dTree.isSelectionEmpty() && dTree.getSelectionPath() != null 465 && // check that this isn't just a model 466 ((TreeNode) dTree.getSelectionPath().getLastPathComponent()).isLeaf() 467 && // can't be just a mfg, has to be at least a family 468 dTree.getSelectionPath().getPathCount() > 2 469 && // can't be a multiple decoder selection 470 dTree.getSelectionCount() < 2) { 471 log.debug("Selection event with {}", dTree.getSelectionPath()); 472 go2.setEnabled(true); 473 go2.setRequestFocusEnabled(true); 474 go2.requestFocus(); 475 go2.setToolTipText(Bundle.getMessage("ToolTipUseSelectedDecoder")); // NOI18N 476 } else { 477 // decoder not selected - require one 478 go2.setEnabled(false); 479 go2.setToolTipText(Bundle.getMessage("ToolTipNoSelectedDecoder")); // NOI18N 480 } 481 }; 482 dTree.addTreeSelectionListener(dListener); 483 dTree.removeFocusListener(fListener); 484 fListener = new FocusListener() { 485 486 /** 487 * Invoked when a component gains the keyboard focus. 488 */ 489 @Override 490 public synchronized void focusGained(FocusEvent e) { 491 log.debug("Focus Gained, {}empty, {}", 492 (dTree.isSelectionEmpty() ? "" : "not "), Arrays.toString(dTree.getSelectionPaths())); 493 setLastActionDisplay("TextManualSelection"); 494 } 495 496 /** 497 * Invoked when a component loses the keyboard focus. 498 */ 499 @Override 500 public void focusLost(FocusEvent e) { 501 log.debug("Focus Lost, {}empty, {}", 502 (dTree.isSelectionEmpty() ? "" : "not "), Arrays.toString(dTree.getSelectionPaths())); 503 setLastActionDisplay("TextManualSelection"); 504 } 505 }; 506 dTree.addFocusListener(fListener); 507 return pan; 508 } 509 510 /** 511 * Identify loco button pressed, start the identify operation. This 512 * defines what happens when the identify is done. 513 * <br><br> 514 * This {@code @Override} method invokes 515 * {@link #setLastActionDisplay setLastActionDisplay} before 516 * starting. 517 */ 518 @Override 519 protected synchronized void startIdentifyDecoder() { 520 // start identifying a decoder 521 setLastActionDisplay("ButtonReadType"); 522 523 super.startIdentifyDecoder(); 524 } 525 526 JToggleButton versionMatchButton() { 527 JToggleButton button = new JToggleButton(Bundle.getMessage("ButtonShowVersionMatch")); 528 button.setToolTipText(Bundle.getMessage("ToolTipVersionMatch", cV7Value, theEntry.getId())); 529 button.addActionListener((java.awt.event.ActionEvent e) -> { 530 resetSelections(); 531 updateForDecoderTypeID(versionMatchList); 532 button.setSelected(false); 533 setLastActionDisplay("ButtonShowVersionMatch"); 534 setShowMatchedOnly(true); 535 }); 536 return button; 537 } 538 539 JToggleButton replacementMatchButton() { 540 JToggleButton button = new JToggleButton(Bundle.getMessage("ButtonShowSuggested")); 541 button.setToolTipText(Bundle.getMessage("ToolTipShowSuggested")); 542 button.addActionListener((java.awt.event.ActionEvent e) -> { 543 resetSelections(); 544 updateForDecoderTypeID(replacementList); 545 button.setSelected(false); 546 setLastActionDisplay("ButtonShowSuggested"); 547 setShowMatchedOnly(true); 548 }); 549 return button; 550 } 551 552 @Override 553 protected synchronized JPanel createProgrammerSelection() { 554 log.debug("createProgrammerSelection"); 555 556 JPanel pane3a = new JPanel(); 557 pane3a.setLayout(new BoxLayout(pane3a, BoxLayout.Y_AXIS)); 558 559 cancelButton = new JButton(Bundle.getMessage("ButtonCancel")); 560 cancelButton.addActionListener((java.awt.event.ActionEvent e) -> { 561 log.debug("Cancel"); // NOI18N 562 log.debug("Closing f {}", f); 563 WindowEvent wev = new WindowEvent(f, WindowEvent.WINDOW_CLOSING); 564 java.awt.Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(wev); 565 f.dispose(); 566 }); 567 cancelButton.setAlignmentX(JLabel.RIGHT_ALIGNMENT); 568 cancelButton.setToolTipText(Bundle.getMessage("ToolTipButtonCancel")); 569 bottomPanel.add(cancelButton, BorderLayout.WEST); 570 571 lastActionDisplay = new JLabel("", SwingConstants.CENTER); 572 bottomPanel.add(lastActionDisplay, BorderLayout.CENTER); 573 574 go2 = new JButton(Bundle.getMessage("ButtonUseSelected")); 575 go2.addActionListener((java.awt.event.ActionEvent e) -> { 576 log.debug("Use Selected pressed"); // NOI18N 577 openButton(); 578 log.debug("Closing f {}", f); 579 WindowEvent wev = new WindowEvent(f, WindowEvent.WINDOW_CLOSING); 580 java.awt.Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(wev); 581 f.dispose(); 582 }); 583 go2.setAlignmentX(JLabel.RIGHT_ALIGNMENT); 584 go2.setEnabled(false); 585 go2.setToolTipText(Bundle.getMessage("ToolTipNoSelectedDecoder")); 586 bottomPanel.add(go2, BorderLayout.EAST); 587 588 return pane3a; //empty pane in this case 589 } 590 591 }; 592 593 // load primary frame 594 // Help panel 595 JPanel helpPane = new JPanel(); 596 JScrollPane helpScroll = new JScrollPane(helpPane); 597 helpPane.setLayout(new BoxLayout(helpPane, BoxLayout.Y_AXIS)); 598 String[] buttons 599 = {"ButtonReadType", "ButtonShowVersionMatch", "ButtonShowSuggested", "ButtonAllMatched", "ButtonUseSelected", "ButtonCancel"}; 600 for (String button : buttons) { 601 JLabel l; 602 l = new JLabel("<html><strong>"" + Bundle.getMessage(button) + ""</strong></html>"); 603 l.setAlignmentX(Component.LEFT_ALIGNMENT); 604 helpPane.add(l); 605 int line = 1; 606 while (line >= 0) { 607 try { 608 String msg = Bundle.getMessage(button + "Help" + line, theEntry.getId()); 609 if (msg.isEmpty()) { 610 msg = " "; 611 } 612 l = new JLabel(msg); 613 l.setAlignmentX(Component.LEFT_ALIGNMENT); 614 helpPane.add(l); 615 line++; 616 } catch (java.util.MissingResourceException e) { // deliberately runs until exception 617 line = -1; 618 } 619 } 620 l = new JLabel(" "); 621 l.setAlignmentX(Component.LEFT_ALIGNMENT); 622 helpPane.add(l); 623 } 624 625 dialogPane.add(helpScroll); 626 627 JPanel infoPane = new JPanel(); 628 JLabel l; 629 l = new JLabel(Bundle.getMessage("TextReplDefn", theEntry.getDecoderFamily(), theEntry.getDecoderModel(), theEntry.getId())); 630 infoPane.add(l); 631 dialogPane.add(infoPane); 632 633 JPanel selectorPane = new JPanel(); 634 selectorPane.setLayout(new BorderLayout()); 635 JPanel topPanel = new JPanel(); 636 topPanel.add(modePane); 637 topPanel.add(new JSeparator(javax.swing.SwingConstants.HORIZONTAL)); 638 selectorPane.add(topPanel, BorderLayout.NORTH); 639 combinedLocoSelTree.setAlignmentX(JLabel.CENTER_ALIGNMENT); 640 selectorPane.add(combinedLocoSelTree, BorderLayout.CENTER); 641 642 statusLabel.setAlignmentX(JLabel.CENTER_ALIGNMENT); 643 bottomPanel.add(statusLabel, BorderLayout.SOUTH); 644 selectorPane.add(bottomPanel, BorderLayout.SOUTH); 645 dialogPane.add(selectorPane, BorderLayout.CENTER); 646 647 f.pack(); 648 log.debug("Tab-Programmer setup created"); // NOI18N 649 650 if (!versionMatchList.isEmpty()) { 651 combinedLocoSelTree.updateForDecoderTypeID(versionMatchList); 652 setLastActionDisplay("ButtonShowVersionMatch"); 653 combinedLocoSelTree.setShowMatchedOnly(true); 654 } else if (!replacementList.isEmpty()) { 655 combinedLocoSelTree.updateForDecoderTypeID(replacementList); 656 setLastActionDisplay("ButtonShowSuggested"); 657 combinedLocoSelTree.setShowMatchedOnly(true); 658 } 659 f.setVisible(true); 660 log.trace("Test done"); 661 } 662 663 /** 664 * Updates the {@link #lastActionDisplay lastActionDisplay} {@link JLabel} 665 * to be the text fetched by the key named "{@code TextLastAction}", after 666 * inclusion of the text fetched by the key named "{@code propertyName}". 667 * 668 * @param propertyName the name of a {@link java.util.ResourceBundle} key 669 */ 670 synchronized void setLastActionDisplay(String propertyName) { 671 this.lastActionDisplay.setText(Bundle.getMessage("TextLastAction", Bundle.getMessage(propertyName))); 672 log.debug("Last Action display changed to {}", this.lastActionDisplay.getText()); 673 } 674 675 /** 676 * Creates two {@link ArrayList ArrayLists} for decoder matching. 677 * <br><br> 678 * They are: 679 * <ul> 680 * <li> 681 * A {@link #replacementList replacementList} based on the combination of 682 * any {@link #replacementFamily replacementFamily} and 683 * {@link #replacementModel replacementModel} suggestions. 684 * </li> 685 * <li> 686 * A {@link #versionMatchList versionMatchList} that is the subset of 687 * {@link #replacementList replacementList} that also matches both a 688 * manufacturerID (from 689 * {@link jmri.jmrit.decoderdefn.IdentifyDecoder} mfgID) 690 * stored in CV8 and a versionID (from 691 * {@link jmri.jmrit.decoderdefn.IdentifyDecoder} modelID) stored 692 * in CV7. 693 * </li> 694 * </ul> 695 * 696 * @param theEntry an existing roster entry that needs replacement 697 */ 698 synchronized void makeMatchLists(RosterEntry theEntry) { 699 versionMatchList = new ArrayList<>(); 700 replacementList = new ArrayList<>(); 701 702 // Get CV values from file. 703 theEntry.readFile(); 704 CvTableModel cvModel = new CvTableModel(null, null); 705 theEntry.loadCvModel(null, cvModel); 706 CvValue cvObject; 707 cV7Value = 0; 708 cvObject = cvModel.allCvMap().get("7"); 709 if (cvObject != null) { 710 cV7Value = cvObject.getValue(); 711 } 712 cV8Value = 0; 713 cvObject = cvModel.allCvMap().get("8"); 714 if (cvObject != null) { 715 cV8Value = cvObject.getValue(); 716 } 717 log.trace("cV7Value = {}, cV8Value = {}", cV7Value, cV8Value); 718 for (String theFamily : replacementFamilyString.split(QRY_SEPARATOR)) { 719 if (theFamily != null && theFamily.equals("")) { 720 theFamily = null; 721 } 722 for (String theModel : replacementModelString.split(QRY_SEPARATOR)) { 723 if (theModel != null && theModel.equals("")) { 724 theModel = null; 725 } 726 log.trace("theFamily = {}, theModel = {}", theFamily, theModel); 727 List<DecoderFile> decoders = di.matchingDecoderList(null, theFamily, null, null, null, theModel); 728 log.trace("Found {} replacement decoders matching family \"{}\" model \"{}\"", 729 decoders.size(), theFamily, theModel); 730 731 for (DecoderFile decoder : decoders) { 732 if ((decoder.getShowable() != DecoderFile.Showable.NO) 733 && !(decoder.getFamily().equals(theEntry.getDecoderFamily()) && decoder.getModel().equals(theEntry.getDecoderModel()))) { 734 if ((cV7Value > 0) && (cV8Value > 0) && decoder.isVersion(cV7Value)) { 735 log.trace("Adding to versionMatchList mfg='{}', family='{}', model='{}'", decoder.getMfg(), decoder.getFamily(), decoder.getModel()); 736 versionMatchList.add(new DecoderFile(decoder.getMfg(), null, decoder.getModel(), 737 null, null, decoder.getFamily(), null, 0, 0, null)); 738 } 739 log.trace("Adding to replacementList mfg='{}', family='{}', model='{}'", decoder.getMfg(), decoder.getFamily(), decoder.getModel()); 740 replacementList.add(new DecoderFile(decoder.getMfg(), null, decoder.getModel(), 741 null, null, decoder.getFamily(), null, 0, 0, null)); 742 } 743 } 744 } 745 } 746 747 updateMatchButtons(theEntry); 748 } 749 750 /** 751 * Updates the {@link #versionButton versionButton} and 752 * {@link #replacementButton replacementButton} availability and tooltips, 753 * depending on whether {@link #versionMatchList versionMatchList} and 754 * {@link #replacementList replacementList} are empty or not. 755 * 756 * @param theEntry an existing roster entry that needs replacement 757 */ 758 synchronized void updateMatchButtons(RosterEntry theEntry) { 759 if (versionButton != null) { 760 if ((versionMatchList == null) || versionMatchList.isEmpty()) { 761 versionButton.setEnabled(false); 762 versionButton.setToolTipText(Bundle.getMessage("ToolTipNoVersionMatch", theEntry.getId())); 763 } else { 764 log.trace("versionMatchList size = {}", versionMatchList.size()); 765 versionButton.setEnabled(true); 766 versionButton.setToolTipText(Bundle.getMessage("ToolTipVersionMatch", cV7Value, theEntry.getId())); 767 } 768 } 769 if (replacementButton != null) { 770 if ((replacementList == null) || replacementList.isEmpty()) { 771 replacementButton.setEnabled(false); 772 replacementButton.setToolTipText(Bundle.getMessage("ToolTipNoShowSuggested")); 773 } else { 774 log.trace("replacementList size = {}", replacementList.size()); 775 replacementButton.setEnabled(true); 776 replacementButton.setToolTipText(Bundle.getMessage("ToolTipShowSuggested")); 777 } 778 } 779 } 780 781 /** 782 * Never invoked, because we overrode actionPerformed above. 783 * 784 * @return never because it deliberately throws an 785 * {@link IllegalArgumentException} 786 */ 787 @Override 788 public synchronized jmri.util.swing.JmriPanel makePanel() { 789 throw new IllegalArgumentException("Should not be invoked"); 790 } 791 // initialize logging 792 private static final Logger log = LoggerFactory.getLogger(UpdateDecoderDefinitionAction.class); 793}