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>&quot;" + Bundle.getMessage(button) + "&quot;</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}