001package jmri.jmrit.roster.swing;
002
003import java.awt.BorderLayout;
004import java.awt.Color;
005import java.awt.Component;
006import java.awt.Cursor;
007import java.awt.Dimension;
008import java.awt.FlowLayout;
009import java.awt.GridBagConstraints;
010import java.awt.GridBagLayout;
011import java.awt.GridLayout;
012import java.awt.Insets;
013import java.awt.datatransfer.Transferable;
014import java.awt.event.ActionEvent;
015import java.awt.event.ActionListener;
016import java.awt.event.WindowEvent;
017import java.awt.image.BufferedImage;
018
019import java.beans.PropertyChangeEvent;
020import java.beans.PropertyChangeListener;
021import java.io.File;
022import java.io.IOException;
023import java.text.DateFormat;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.List;
027
028import javax.annotation.CheckForNull;
029import javax.imageio.ImageIO;
030import javax.swing.*;
031import javax.swing.event.ListSelectionEvent;
032
033import jmri.AddressedProgrammerManager;
034import jmri.GlobalProgrammerManager;
035import jmri.InstanceManager;
036import jmri.Programmer;
037import jmri.ShutDownManager;
038import jmri.UserPreferencesManager;
039import jmri.jmrit.decoderdefn.DecoderFile;
040import jmri.jmrit.decoderdefn.DecoderIndexFile;
041import jmri.jmrit.progsupport.ProgModeSelector;
042import jmri.jmrit.progsupport.ProgServiceModeComboBox;
043import jmri.jmrit.roster.CopyRosterItemAction;
044import jmri.jmrit.roster.DeleteRosterItemAction;
045import jmri.jmrit.roster.ExportRosterItemAction;
046import jmri.jmrit.roster.IdentifyLoco;
047import jmri.jmrit.roster.PrintRosterEntry;
048import jmri.jmrit.roster.Roster;
049import jmri.jmrit.roster.RosterEntry;
050import jmri.jmrit.roster.RosterEntrySelector;
051import jmri.jmrit.roster.rostergroup.RosterGroupSelector;
052import jmri.jmrit.symbolicprog.ProgrammerConfigManager;
053import jmri.jmrit.symbolicprog.tabbedframe.PaneOpsProgFrame;
054import jmri.jmrit.symbolicprog.tabbedframe.PaneProgFrame;
055import jmri.jmrit.symbolicprog.tabbedframe.PaneServiceProgFrame;
056import jmri.jmrit.throttle.LargePowerManagerButton;
057import jmri.jmrit.throttle.ThrottleFrame;
058import jmri.jmrit.throttle.ThrottleFrameManager;
059import jmri.jmrix.ActiveSystemsMenu;
060import jmri.jmrix.ConnectionConfig;
061import jmri.jmrix.ConnectionConfigManager;
062import jmri.jmrix.ConnectionStatus;
063import jmri.profile.Profile;
064import jmri.profile.ProfileManager;
065import jmri.swing.JTablePersistenceManager;
066import jmri.swing.RowSorterUtil;
067import jmri.util.FileUtil;
068import jmri.util.HelpUtil;
069import jmri.util.WindowMenu;
070import jmri.util.datatransfer.RosterEntrySelection;
071import jmri.util.swing.JmriAbstractAction;
072import jmri.util.swing.JmriJOptionPane;
073import jmri.util.swing.JmriMouseAdapter;
074import jmri.util.swing.JmriMouseEvent;
075import jmri.util.swing.JmriMouseListener;
076import jmri.util.swing.ResizableImagePanel;
077import jmri.util.swing.WindowInterface;
078import jmri.util.swing.multipane.TwoPaneTBWindow;
079
080/**
081 * A window for Roster management.
082 * <p>
083 * TODO: Several methods are copied from PaneProgFrame and should be refactored
084 * No programmer support yet (dummy object below). Color only covering borders.
085 * No reset toolbar support yet. No glass pane support (See DecoderPro3Panes
086 * class and usage below). Special panes (Roster entry, attributes, graphics)
087 * not included. How do you pick a programmer file? (hardcoded) Initialization
088 * needs partial deferral, too for 1st pane to appear.
089 *
090 * @see jmri.jmrit.symbolicprog.tabbedframe.PaneSet
091 *
092 * @author Bob Jacobsen Copyright (C) 2010, 2016
093 * @author Kevin Dickerson Copyright (C) 2011
094 * @author Randall Wood Copyright (C) 2012
095 */
096public class RosterFrame extends TwoPaneTBWindow implements RosterEntrySelector, RosterGroupSelector {
097
098    static final ArrayList<RosterFrame> frameInstances = new ArrayList<>();
099    protected boolean allowQuit = true;
100    protected String baseTitle = "Roster";
101    protected JmriAbstractAction newWindowAction;
102
103    public RosterFrame() {
104        this(Bundle.getMessage("RosterTitle"));
105    }
106
107    public RosterFrame(String name) {
108        this(name,
109                "xml/config/parts/jmri/jmrit/roster/swing/RosterFrameMenu.xml",
110                "xml/config/parts/jmri/jmrit/roster/swing/RosterFrameToolBar.xml");
111    }
112
113    public RosterFrame(String name, String menubarFile, String toolbarFile) {
114        super(name, menubarFile, toolbarFile);
115        this.allowInFrameServlet = false;
116        this.setBaseTitle(name);
117        this.buildWindow();
118    }
119
120    final JRadioButtonMenuItem contextEdit = new JRadioButtonMenuItem(Bundle.getMessage("EditOnly"));
121    final JRadioButtonMenuItem contextOps = new JRadioButtonMenuItem(Bundle.getMessage("ProgrammingOnMain"));
122    final JRadioButtonMenuItem contextService = new JRadioButtonMenuItem(Bundle.getMessage("ProgrammingTrack"));
123    final JTextPane dateUpdated = new JTextPane();
124    final JTextPane dccAddress = new JTextPane();
125    final JTextPane decoderFamily = new JTextPane();
126    final JTextPane decoderModel = new JTextPane();
127    final JRadioButton edit = new JRadioButton(Bundle.getMessage("EditOnly"));
128    final JTextPane filename = new JTextPane();
129    JLabel firstHelpLabel;
130    //int firstTimeAddedEntry = 0x00;
131    int groupSplitPaneLocation = 0;
132    RosterGroupsPanel groups;
133    boolean hideGroups = false;
134    boolean hideRosterImage = false;
135    final JTextPane id = new JTextPane();
136    boolean inStartProgrammer = false;
137    ResizableImagePanel locoImage;
138    JTextPane maxSpeed = new JTextPane();
139    final JTextPane mfg = new JTextPane();
140    final ProgModeSelector modePanel = new ProgServiceModeComboBox();
141    final JTextPane model = new JTextPane();
142    final JLabel operationsModeProgrammerLabel = new JLabel();
143    final JRadioButton ops = new JRadioButton(Bundle.getMessage("ProgrammingOnMain"));
144    ConnectionConfig opsModeProCon = null;
145    final JTextPane owner = new JTextPane();
146    UserPreferencesManager prefsMgr;
147    final JButton prog1Button = new JButton(Bundle.getMessage("Program"));
148    final JButton prog2Button = new JButton(Bundle.getMessage("BasicProgrammer"));
149    ActionListener programModeListener;
150
151    // These are the names of the programmer _files_, not what should be displayed to the user
152    String programmer1 = "Comprehensive"; // NOI18N
153    String programmer2 = "Basic"; // NOI18N
154
155    final java.util.ResourceBundle rb = java.util.ResourceBundle.getBundle("apps.AppsBundle");
156    //current selected loco
157    transient RosterEntry re;
158    final JTextPane roadName = new JTextPane();
159    final JTextPane roadNumber = new JTextPane();
160    final JPanel rosterDetailPanel = new JPanel();
161    PropertyChangeListener rosterEntryUpdateListener;
162    JSplitPane rosterGroupSplitPane;
163    final JButton rosterMedia = new JButton(Bundle.getMessage("LabelsAndMedia"));
164    RosterTable rtable;
165    ConnectionConfig serModeProCon = null;
166    final JRadioButton service = new JRadioButton(Bundle.getMessage("ProgrammingTrack"));
167    final JLabel serviceModeProgrammerLabel = new JLabel();
168    final JLabel statusField = new JLabel();
169    final Dimension summaryPaneDim = new Dimension(0, 170);
170    final JButton throttleLabels = new JButton(Bundle.getMessage("ThrottleLabels"));
171    final JButton throttleLaunch = new JButton(Bundle.getMessage("Throttle"));
172
173    protected void additionsToToolBar() {
174        getToolBar().add(new LargePowerManagerButton(true));
175        getToolBar().add(Box.createHorizontalGlue());
176        JPanel p = new JPanel();
177        p.setAlignmentX(JPanel.RIGHT_ALIGNMENT);
178        p.add(modePanel);
179        getToolBar().add(p);
180    }
181
182    /**
183     * For use when the DP3 window is called from another JMRI instance, set
184     * this to prevent the DP3 from shutting down JMRI when the window is
185     * closed.
186     *
187     * @param quitAllowed true if closing window should quit application; false
188     *                    otherwise
189     */
190    protected void allowQuit(boolean quitAllowed) {
191        if (allowQuit != quitAllowed) {
192            newWindowAction = null;
193            allowQuit = quitAllowed;
194            groups.setNewWindowMenuAction(this.getNewWindowAction());
195        }
196
197        firePropertyChange("quit", "setEnabled", allowQuit);
198        //if we are not allowing quit, ie opened from JMRI classic
199        //then we must at least allow the window to be closed
200        if (!allowQuit) {
201            firePropertyChange("closewindow", "setEnabled", true);
202        }
203    }
204
205    JPanel bottomRight() {
206        JPanel panel = new JPanel();
207        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
208        ButtonGroup progMode = new ButtonGroup();
209        progMode.add(service);
210        progMode.add(ops);
211        progMode.add(edit);
212        service.setEnabled(false);
213        ops.setEnabled(false);
214        edit.setEnabled(true);
215        firePropertyChange("setprogservice", "setEnabled", false);
216        firePropertyChange("setprogops", "setEnabled", false);
217        firePropertyChange("setprogedit", "setEnabled", true);
218        ops.setOpaque(false);
219        service.setOpaque(false);
220        edit.setOpaque(false);
221        JPanel progModePanel = new JPanel();
222        GridLayout buttonLayout = new GridLayout(3, 1, 0, 0);
223        progModePanel.setLayout(buttonLayout);
224        progModePanel.add(service);
225        progModePanel.add(ops);
226        progModePanel.add(edit);
227        programModeListener = (ActionEvent e) -> updateProgMode();
228        service.addActionListener(programModeListener);
229        ops.addActionListener(programModeListener);
230        edit.addActionListener(programModeListener);
231        service.setVisible(false);
232        ops.setVisible(false);
233        panel.add(progModePanel);
234        JPanel buttonHolder = new JPanel(new GridBagLayout());
235        GridBagConstraints c = new GridBagConstraints();
236        c.weightx = 1.0;
237        c.fill = GridBagConstraints.HORIZONTAL;
238        c.anchor = GridBagConstraints.NORTH;
239        c.gridx = 0;
240        c.ipady = 20;
241        c.gridwidth = GridBagConstraints.REMAINDER;
242        c.gridy = 0;
243        c.insets = new Insets(2, 2, 2, 2);
244        buttonHolder.add(prog1Button, c);
245        c.weightx = 1;
246        c.fill = GridBagConstraints.NONE;
247        c.gridx = 0;
248        c.gridy = 1;
249        c.gridwidth = 1;
250        c.ipady = 0;
251        buttonHolder.add(rosterMedia, c);
252        c.weightx = 1.0;
253        c.fill = GridBagConstraints.NONE;
254        c.gridx = 1;
255        c.gridy = 1;
256        c.gridwidth = 1;
257        c.ipady = 0;
258        buttonHolder.add(throttleLaunch, c);
259        //buttonHolder.add(throttleLaunch);
260        panel.add(buttonHolder);
261        prog1Button.setEnabled(false);
262        prog1Button.addActionListener((ActionEvent e) -> {
263            log.debug("Open programmer pressed");
264            startProgrammer(null, re, programmer1);
265        });
266
267        rosterMedia.setEnabled(false);
268        rosterMedia.addActionListener((ActionEvent e) -> {
269            log.debug("Open Media pressed");
270            edit.setSelected(true);
271            startProgrammer(null, re, "dp3" + File.separator + "MediaPane");
272        });
273        throttleLaunch.setEnabled(false);
274        throttleLaunch.addActionListener((ActionEvent e) -> {
275            log.debug("Launch Throttle pressed");
276            if (!checkIfEntrySelected()) {
277                return;
278            }
279            ThrottleFrame tf = InstanceManager.getDefault(ThrottleFrameManager.class).createThrottleFrame();
280            tf.toFront();
281            tf.getAddressPanel().setRosterEntry(re);
282        });
283        return panel;
284    }
285
286    protected final void buildWindow() {
287        //Additions to the toolbar need to be added first otherwise when trying to hide bits up during the initialisation they remain on screen
288        additionsToToolBar();
289        frameInstances.add(this);
290        prefsMgr = InstanceManager.getDefault(UserPreferencesManager.class);
291        getTop().add(createTop());
292        getBottom().setMinimumSize(summaryPaneDim);
293        getBottom().add(createBottom());
294        statusBar();
295        systemsMenu();
296        helpMenu(getMenu(), this);
297        if ((!prefsMgr.getSimplePreferenceState(this.getClass().getName() + ".hideGroups")) && !Roster.getDefault().getRosterGroupList().isEmpty()) {
298            hideGroupsPane(false);
299        } else {
300            hideGroupsPane(true);
301        }
302        if (prefsMgr.getSimplePreferenceState(this.getClass().getName() + ".hideSummary")) {
303            //We have to set it to display first, then we can hide it.
304            hideBottomPane(false);
305            hideBottomPane(true);
306        }
307        PropertyChangeListener propertyChangeListener = (PropertyChangeEvent changeEvent) -> {
308            JSplitPane sourceSplitPane = (JSplitPane) changeEvent.getSource();
309            String propertyName = changeEvent.getPropertyName();
310            if (propertyName.equals(JSplitPane.LAST_DIVIDER_LOCATION_PROPERTY)) {
311                int current = sourceSplitPane.getDividerLocation() + sourceSplitPane.getDividerSize();
312                int panesize = (int) (sourceSplitPane.getSize().getHeight());
313                hideBottomPane = panesize - current <= 1;
314                //p.setSimplePreferenceState(DecoderPro3Window.class.getName()+".hideSummary",hideSummary);
315            }
316        };
317        updateProgrammerStatus(null);
318        ConnectionStatus.instance().addPropertyChangeListener((PropertyChangeEvent e) -> {
319            if ((e.getPropertyName().equals("change")) || (e.getPropertyName().equals("add"))) {
320                log.debug("Received property {} with value {} ", e.getPropertyName(), e.getNewValue());
321                updateProgrammerStatus(e);
322            }
323        });
324        InstanceManager.addPropertyChangeListener(InstanceManager.getListPropertyName(AddressedProgrammerManager.class),
325                evt -> {
326                    log.debug("Received property {} with value {} ", evt.getPropertyName(), evt.getNewValue());
327                    AddressedProgrammerManager m = (AddressedProgrammerManager) evt.getNewValue();
328                    if (m != null) {
329                        m.addPropertyChangeListener(this::updateProgrammerStatus);
330                    }
331                    updateProgrammerStatus(evt);
332                });
333        InstanceManager.getList(AddressedProgrammerManager.class).forEach(m -> m.addPropertyChangeListener(this::updateProgrammerStatus));
334        InstanceManager.addPropertyChangeListener(InstanceManager.getListPropertyName(GlobalProgrammerManager.class),
335                evt -> {
336                    log.debug("Received property {} with value {} ", evt.getPropertyName(), evt.getNewValue());
337                    GlobalProgrammerManager m = (GlobalProgrammerManager) evt.getNewValue();
338                    if (m != null) {
339                        m.addPropertyChangeListener(this::updateProgrammerStatus);
340                    }
341                    updateProgrammerStatus(evt);
342                });
343        InstanceManager.getList(GlobalProgrammerManager.class).forEach(m -> m.addPropertyChangeListener(this::updateProgrammerStatus));
344        getSplitPane().addPropertyChangeListener(propertyChangeListener);
345        if (this.getProgrammerConfigManager().getDefaultFile() != null) {
346            programmer1 = this.getProgrammerConfigManager().getDefaultFile();
347        }
348        this.getProgrammerConfigManager().addPropertyChangeListener(ProgrammerConfigManager.DEFAULT_FILE, (PropertyChangeEvent evt) -> {
349            if (this.getProgrammerConfigManager().getDefaultFile() != null) {
350                programmer1 = this.getProgrammerConfigManager().getDefaultFile();
351            }
352        });
353
354        String lastProg = (String) prefsMgr.getProperty(getWindowFrameRef(), "selectedProgrammer");
355        if (lastProg != null) {
356            if (lastProg.equals("service") && service.isEnabled()) {
357                service.setSelected(true);
358                updateProgMode();
359            } else if (lastProg.equals("ops") && ops.isEnabled()) {
360                ops.setSelected(true);
361                updateProgMode();
362            } else if (lastProg.equals("edit") && edit.isEnabled()) {
363                edit.setSelected(true);
364                updateProgMode();
365            }
366        }
367        if (frameInstances.size() > 1) {
368            firePropertyChange("closewindow", "setEnabled", true);
369            allowQuit(frameInstances.get(0).isAllowQuit());
370        } else {
371            firePropertyChange("closewindow", "setEnabled", false);
372        }
373    }
374
375    boolean checkIfEntrySelected() {
376        return this.checkIfEntrySelected(false);
377    }
378
379    boolean checkIfEntrySelected(boolean allowMultiple) {
380        if ((re == null && !allowMultiple) || (this.getSelectedRosterEntries().length < 1)) {
381            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("ErrorNoSelection"));
382            return false;
383        }
384        return true;
385    }
386
387    //@TODO The disabling of the closeWindow menu item doesn't quite work as this in only invoked on the closing window, and not the one that is left
388    void closeWindow(WindowEvent e) {
389        saveWindowDetails();
390        //Save any changes made in the roster entry details
391        Roster.getDefault().writeRoster();
392        if (allowQuit && frameInstances.size() == 1 && !InstanceManager.getDefault(ShutDownManager.class).isShuttingDown()) {
393            handleQuit(e);
394        } else {
395            //As we are not the last window open or we are not allowed to quit the application then we will just close the current window
396            frameInstances.remove(this);
397            super.windowClosing(e);
398            if ((frameInstances.size() == 1) && (allowQuit)) {
399                frameInstances.get(0).firePropertyChange("closewindow", "setEnabled", false);
400            }
401            dispose();
402        }
403    }
404
405    protected void copyLoco() {
406        CopyRosterItem act = new CopyRosterItem("Copy", this, re);
407        act.actionPerformed(null);
408    }
409
410    JComponent createBottom() {
411        locoImage = new ResizableImagePanel(null, 240, 160);
412        locoImage.setBorder(BorderFactory.createLineBorder(Color.blue));
413        locoImage.setOpaque(true);
414        locoImage.setRespectAspectRatio(true);
415        rosterDetailPanel.setLayout(new BorderLayout());
416        rosterDetailPanel.add(locoImage, BorderLayout.WEST);
417        rosterDetailPanel.add(rosterDetails(), BorderLayout.CENTER);
418        rosterDetailPanel.add(bottomRight(), BorderLayout.EAST);
419        if (prefsMgr.getSimplePreferenceState(this.getClass().getName() + ".hideRosterImage")) {
420            locoImage.setVisible(false);
421            hideRosterImage = true;
422        }
423        rosterEntryUpdateListener = (PropertyChangeEvent e) -> updateDetails();
424        return rosterDetailPanel;
425    }
426
427    JComponent createTop() {
428        Object selectedRosterGroup = prefsMgr.getProperty(getWindowFrameRef(), SELECTED_ROSTER_GROUP);
429        groups = new RosterGroupsPanel((selectedRosterGroup != null) ? selectedRosterGroup.toString() : null);
430        groups.setNewWindowMenuAction(this.getNewWindowAction());
431        setTitle(groups.getSelectedRosterGroup());
432        final JPanel rosters = new JPanel();
433        rosters.setLayout(new BorderLayout());
434        // set up roster table
435        rtable = new RosterTable(true, ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
436        rtable.setRosterGroup(this.getSelectedRosterGroup());
437        rtable.setRosterGroupSource(groups);
438        rosters.add(rtable, BorderLayout.CENTER);
439        // add selection listener
440        rtable.getTable().getSelectionModel().addListSelectionListener((ListSelectionEvent e) -> {
441            JTable table = rtable.getTable();
442            if (!e.getValueIsAdjusting()) {
443                if (rtable.getSelectedRosterEntries().length == 1 && table.getSelectedRow() >= 0) {
444                    log.debug("Selected row {}", table.getSelectedRow());
445                    locoSelected(rtable.getModel().getValueAt(table.getRowSorter().convertRowIndexToModel(table.getSelectedRow()), RosterTableModel.IDCOL).toString());
446                } else if (rtable.getSelectedRosterEntries().length > 1 || table.getSelectedRow() < 0) {
447                    locoSelected(null);
448                } // leave last selected item visible if no selection
449            }
450        });
451
452        //Set all the sort and width details of the table first.
453        String rostertableref = getWindowFrameRef() + ":roster";
454        rtable.getTable().setName(rostertableref);
455
456        // Allow only one column to be sorted at a time -
457        // Java allows multiple column sorting, but to effectively persist that, we
458        // need to be intelligent about which columns can be meaningfully sorted
459        // with other columns; this bypasses the problem by only allowing the
460        // last column sorted to affect sorting
461        RowSorterUtil.addSingleSortableColumnListener(rtable.getTable().getRowSorter());
462
463        // Reset and then persist the table's ui state
464        JTablePersistenceManager tpm = InstanceManager.getNullableDefault(JTablePersistenceManager.class);
465        if (tpm != null) {
466            tpm.resetState(rtable.getTable());
467            tpm.persist(rtable.getTable());
468        }
469        rtable.getTable().setDragEnabled(true);
470        rtable.getTable().setTransferHandler(new TransferHandler() {
471
472            @Override
473            public int getSourceActions(JComponent c) {
474                return TransferHandler.COPY;
475            }
476
477            @Override
478            public Transferable createTransferable(JComponent c) {
479                JTable table = rtable.getTable();
480                ArrayList<String> Ids = new ArrayList<>(table.getSelectedRowCount());
481                for (int i = 0; i < table.getSelectedRowCount(); i++) {
482                    Ids.add(rtable.getModel().getValueAt(table.getRowSorter().convertRowIndexToModel(table.getSelectedRows()[i]), RosterTableModel.IDCOL).toString());
483                }
484                return new RosterEntrySelection(Ids);
485            }
486
487            @Override
488            public void exportDone(JComponent c, Transferable t, int action) {
489                // nothing to do
490            }
491        });
492        JmriMouseListener rosterMouseListener = new RosterPopupListener();
493        rtable.getTable().addMouseListener(JmriMouseListener.adapt(rosterMouseListener));
494
495        // assemble roster/groups splitpane
496        rosterGroupSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, groups, rosters);
497        rosterGroupSplitPane.setOneTouchExpandable(true);
498        rosterGroupSplitPane.setResizeWeight(0); // emphasis rosters
499        Object w = prefsMgr.getProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation");
500        if (w != null) {
501            groupSplitPaneLocation = (Integer) w;
502            rosterGroupSplitPane.setDividerLocation(groupSplitPaneLocation);
503        }
504        if (!Roster.getDefault().getRosterGroupList().isEmpty()) {
505            if (prefsMgr.getSimplePreferenceState(this.getClass().getName() + ".hideGroups")) {
506                hideGroupsPane(true);
507            }
508        } else {
509            enableRosterGroupMenuItems(false);
510        }
511        PropertyChangeListener propertyChangeListener = (PropertyChangeEvent changeEvent) -> {
512            JSplitPane sourceSplitPane = (JSplitPane) changeEvent.getSource();
513            String propertyName = changeEvent.getPropertyName();
514            if (propertyName.equals(JSplitPane.LAST_DIVIDER_LOCATION_PROPERTY)) {
515                int current = sourceSplitPane.getDividerLocation();
516                hideGroups = current <= 1;
517                Integer last = (Integer) changeEvent.getNewValue();
518                if (current >= 2) {
519                    groupSplitPaneLocation = current;
520                } else if (last >= 2) {
521                    groupSplitPaneLocation = last;
522                }
523            }
524        };
525        groups.addPropertyChangeListener(SELECTED_ROSTER_GROUP, new PropertyChangeListener() {
526            @Override
527            public void propertyChange(PropertyChangeEvent pce) {
528                prefsMgr.setProperty(this.getClass().getName(), SELECTED_ROSTER_GROUP, pce.getNewValue());
529                setTitle((String) pce.getNewValue());
530            }
531        });
532        rosterGroupSplitPane.addPropertyChangeListener(propertyChangeListener);
533        Roster.getDefault().addPropertyChangeListener((PropertyChangeEvent e) -> {
534            if (e.getPropertyName().equals("RosterGroupAdded") && Roster.getDefault().getRosterGroupList().size() == 1) {
535                // if the pane is hidden, show it when 1st group is created
536                hideGroupsPane(false);
537                enableRosterGroupMenuItems(true);
538            } else if (!rtable.isVisible() && (e.getPropertyName().equals("saved"))) {
539                if (firstHelpLabel != null) {
540                    firstHelpLabel.setVisible(false);
541                }
542                rtable.setVisible(true);
543                rtable.resetColumnWidths();
544            }
545        });
546        if (Roster.getDefault().numEntries() == 0) {
547            try {
548                BufferedImage myPicture = ImageIO.read(FileUtil.findURL(("resources/" + Bundle.getMessage("ThrottleFirstUseImage")), FileUtil.Location.INSTALLED));
549                //rosters.add(new JLabel(new ImageIcon( myPicture )), BorderLayout.CENTER);
550                firstHelpLabel = new JLabel(new ImageIcon(myPicture));
551                rtable.setVisible(false);
552                rosters.add(firstHelpLabel, BorderLayout.NORTH);
553                //tableArea.add(firstHelpLabel);
554                rtable.setVisible(false);
555            } catch (IOException ex) {
556                // handle exception...
557            }
558        }
559        return rosterGroupSplitPane;
560    }
561
562    protected void deleteLoco() {
563        DeleteRosterItemAction act = new DeleteRosterItemAction("Delete", (WindowInterface) this);
564        act.actionPerformed(null);
565    }
566
567    void editMediaButton() {
568        //Because of the way that programmers work, we need to use edit mode for displaying the media pane, so that the read/write buttons do not appear.
569        boolean serviceSelected = service.isSelected();
570        boolean opsSelected = ops.isSelected();
571        edit.setSelected(true);
572        startProgrammer(null, re, "dp3" + File.separator + "MediaPane");
573        service.setSelected(serviceSelected);
574        ops.setSelected(opsSelected);
575    }
576
577    protected void enableRosterGroupMenuItems(boolean enable) {
578        firePropertyChange("groupspane", "setEnabled", enable);
579        firePropertyChange("grouptable", "setEnabled", enable);
580        firePropertyChange("deletegroup", "setEnabled", enable);
581    }
582
583    protected void exportLoco() {
584        ExportRosterItem act = new ExportRosterItem(Bundle.getMessage("Export"), this, re);
585        act.actionPerformed(null);
586    }
587
588    void formatTextAreaAsLabel(JTextPane pane) {
589        pane.setOpaque(false);
590        pane.setEditable(false);
591        pane.setBorder(null);
592    }
593
594    /*=============== Getters and Setters for core properties ===============*/
595
596    /**
597     * @return Will closing the window quit JMRI?
598     */
599    public boolean isAllowQuit() {
600        return allowQuit;
601    }
602
603    /**
604     * @param allowQuit Set state to either close JMRI or just the roster window
605     */
606    public void setAllowQuit(boolean allowQuit) {
607        allowQuit(allowQuit);
608    }
609
610    /**
611     * @return the baseTitle
612     */
613    protected String getBaseTitle() {
614        return baseTitle;
615    }
616
617    /**
618     * @param baseTitle the baseTitle to set
619     */
620    protected final void setBaseTitle(String baseTitle) {
621        String title = null;
622        if (this.baseTitle == null) {
623            title = this.getTitle();
624        }
625        this.baseTitle = baseTitle;
626        if (title != null) {
627            this.setTitle(title);
628        }
629    }
630
631    /**
632     * @return the newWindowAction
633     */
634    protected JmriAbstractAction getNewWindowAction() {
635        if (newWindowAction == null) {
636            newWindowAction = new RosterFrameAction("newWindow", this, allowQuit);
637        }
638        return newWindowAction;
639    }
640
641    /**
642     * @param newWindowAction the newWindowAction to set
643     */
644    protected void setNewWindowAction(JmriAbstractAction newWindowAction) {
645        this.newWindowAction = newWindowAction;
646        this.groups.setNewWindowMenuAction(newWindowAction);
647    }
648
649    @Override
650    public void setTitle(String title) {
651        if (title == null || title.isEmpty()) {
652            title = Roster.ALLENTRIES;
653        }
654        if (this.baseTitle != null) {
655            if (!title.equals(this.baseTitle) && !title.startsWith(this.baseTitle)) {
656                super.setTitle(this.baseTitle + ": " + title);
657            }
658        } else {
659            super.setTitle(title);
660        }
661    }
662
663    @Override
664    public Object getProperty(String key) {
665        if (key.equalsIgnoreCase(SELECTED_ROSTER_GROUP)) {
666            return getSelectedRosterGroup();
667        } else if (key.equalsIgnoreCase("hideSummary")) {
668            return hideBottomPane;
669        }
670        // call parent getProperty method to return any properties defined
671        // in the class hierarchy.
672        return super.getProperty(key);
673    }
674
675    public Object getRemoteObject(String value) {
676        return getProperty(value);
677    }
678
679    @Override
680    public RosterEntry[] getSelectedRosterEntries() {
681        RosterEntry[] entries = rtable.getSelectedRosterEntries();
682        return Arrays.copyOf(entries, entries.length);
683    }
684
685    public RosterEntry[] getAllRosterEntries() {
686        RosterEntry[] entries = rtable.getSortedRosterEntries();
687        return Arrays.copyOf(entries, entries.length);
688    }
689
690    @Override
691    public String getSelectedRosterGroup() {
692        return groups.getSelectedRosterGroup();
693    }
694
695    protected ProgrammerConfigManager getProgrammerConfigManager() {
696        return InstanceManager.getDefault(ProgrammerConfigManager.class);
697    }
698
699    void handleQuit(WindowEvent e) {
700        if (e != null && frameInstances.size() == 1) {
701            final String rememberWindowClose = this.getClass().getName() + ".closeDP3prompt";
702            if (!prefsMgr.getSimplePreferenceState(rememberWindowClose)) {
703                JPanel message = new JPanel();
704                JLabel question = new JLabel(rb.getString("MessageLongCloseWarning"));
705                final JCheckBox remember = new JCheckBox(rb.getString("MessageRememberSetting"));
706                remember.setFont(remember.getFont().deriveFont(10.0F));
707                message.setLayout(new BoxLayout(message, BoxLayout.Y_AXIS));
708                message.add(question);
709                message.add(remember);
710                int result = JmriJOptionPane.showConfirmDialog(null,
711                        message,
712                        rb.getString("MessageShortCloseWarning"),
713                        JmriJOptionPane.YES_NO_OPTION);
714                if (remember.isSelected()) {
715                    prefsMgr.setSimplePreferenceState(rememberWindowClose, true);
716                }
717                if (result == JmriJOptionPane.YES_OPTION) {
718                    handleQuit();
719                }
720            } else {
721                handleQuit();
722            }
723        } else if (frameInstances.size() > 1) {
724            final String rememberWindowClose = this.getClass().getName() + ".closeMultipleDP3prompt";
725            if (!prefsMgr.getSimplePreferenceState(rememberWindowClose)) {
726                JPanel message = new JPanel();
727                JLabel question = new JLabel(rb.getString("MessageLongMultipleCloseWarning"));
728                final JCheckBox remember = new JCheckBox(rb.getString("MessageRememberSetting"));
729                remember.setFont(remember.getFont().deriveFont(10.0F));
730                message.setLayout(new BoxLayout(message, BoxLayout.Y_AXIS));
731                message.add(question);
732                message.add(remember);
733                int result = JmriJOptionPane.showConfirmDialog(null,
734                        message,
735                        rb.getString("MessageShortCloseWarning"),
736                        JmriJOptionPane.YES_NO_OPTION);
737                if (remember.isSelected()) {
738                    prefsMgr.setSimplePreferenceState(rememberWindowClose, true);
739                }
740                if (result == JmriJOptionPane.YES_OPTION) {
741                    handleQuit();
742                }
743            } else {
744                handleQuit();
745            }
746            //closeWindow(null);
747        }
748    }
749
750    private void handleQuit(){
751        try {
752            InstanceManager.getDefault(jmri.ShutDownManager.class).shutdown();
753        } catch (Exception e) {
754            log.error("Continuing after error in handleQuit", e);
755        }
756    }
757
758    protected void helpMenu(JMenuBar menuBar, final JFrame frame) {
759        // create menu and standard items
760        JMenu helpMenu = HelpUtil.makeHelpMenu("package.apps.gui3.dp3.DecoderPro3", true);
761        // use as main help menu
762        menuBar.add(helpMenu);
763    }
764
765    protected void hideGroups() {
766        boolean boo = !hideGroups;
767        hideGroupsPane(boo);
768    }
769
770    public void hideGroupsPane(boolean hide) {
771        if (hideGroups == hide) {
772            return;
773        }
774        hideGroups = hide;
775        if (hide) {
776            groupSplitPaneLocation = rosterGroupSplitPane.getDividerLocation();
777            rosterGroupSplitPane.setDividerLocation(1);
778            rosterGroupSplitPane.getLeftComponent().setMinimumSize(new Dimension());
779            if (Roster.getDefault().getRosterGroupList().isEmpty()) {
780                rosterGroupSplitPane.setOneTouchExpandable(false);
781                rosterGroupSplitPane.setDividerSize(0);
782            }
783        } else {
784            rosterGroupSplitPane.setDividerSize(UIManager.getInt("SplitPane.dividerSize"));
785            rosterGroupSplitPane.setOneTouchExpandable(true);
786            if (groupSplitPaneLocation >= 2) {
787                rosterGroupSplitPane.setDividerLocation(groupSplitPaneLocation);
788            } else {
789                rosterGroupSplitPane.resetToPreferredSizes();
790            }
791        }
792    }
793
794    protected void hideRosterImage() {
795        hideRosterImage = !hideRosterImage;
796        //p.setSimplePreferenceState(DecoderPro3Window.class.getName()+".hideRosterImage",hideRosterImage);
797        if (hideRosterImage) {
798            locoImage.setVisible(false);
799        } else {
800            locoImage.setVisible(true);
801        }
802    }
803
804    protected void hideSummary() {
805        boolean boo = !hideBottomPane;
806        hideBottomPane(boo);
807    }
808
809    /**
810     * An entry has been selected in the Roster Table, activate the bottom part
811     * of the window.
812     *
813     * @param id ID of the selected roster entry
814     */
815    void locoSelected(String id) {
816        if (id != null) {
817            log.debug("locoSelected ID {}", id);
818            if (re != null) {
819                // we remove the propertychangelistener if we had a previously selected entry;
820                re.removePropertyChangeListener(rosterEntryUpdateListener);
821            }
822            // convert to roster entry
823            re = Roster.getDefault().entryFromTitle(id);
824            re.addPropertyChangeListener(rosterEntryUpdateListener);
825        } else {
826            log.debug("Multiple selection");
827            re = null;
828        }
829        updateDetails();
830    }
831
832    protected void newWindow() {
833        this.newWindow(this.getNewWindowAction());
834    }
835
836    protected void newWindow(JmriAbstractAction action) {
837        action.setWindowInterface(this);
838        action.actionPerformed(null);
839        firePropertyChange("closewindow", "setEnabled", true);
840    }
841
842    /**
843     * Prepare a roster entry to be printed, and display a selection list.
844     *
845     * @see jmri.jmrit.roster.PrintRosterEntry#printPanes(boolean)
846     * @param preview true if output should go to a Preview pane on screen, false
847     *            to output to a printer (dialog)
848     */
849    protected void printLoco(boolean preview) {
850        log.debug("Selected entry: {}", re.getDisplayName());
851        String programmer = "Basic";
852        if (this.getProgrammerConfigManager().getDefaultFile() != null) {
853            programmer = this.getProgrammerConfigManager().getDefaultFile();
854        } else {
855            log.error("programmer is NULL");
856        }
857        PrintRosterEntry pre = new PrintRosterEntry(re, this, "programmers" + File.separator + programmer + ".xml");
858        // uses programmer set in prefs when printing a selected entry from (this) top Roster frame
859        // compare with: jmri.jmrit.symbolicprog.tabbedframe.PaneProgFrame#printPanes(boolean)
860        // as user expects to see more tabs on printout using Comprehensive or just 1 tab for Basic programmer
861        pre.printPanes(preview);
862    }
863
864    /**
865     * Match the first argument in the array against a locally-known method.
866     *
867     * @param args Array of arguments, we take with element 0
868     */
869    @Override
870    public void remoteCalls(String[] args) {
871        args[0] = args[0].toLowerCase();
872        switch (args[0]) {
873            case "identifyloco":
874                startIdentifyLoco();
875                break;
876            case "printloco":
877                if (checkIfEntrySelected()) {
878                    printLoco(false);
879                }
880                break;
881            case "printpreviewloco":
882                if (checkIfEntrySelected()) {
883                    printLoco(true);
884                }
885                break;
886            case "exportloco":
887                if (checkIfEntrySelected()) {
888                    exportLoco();
889                }
890                break;
891            case "basicprogrammer":
892                if (checkIfEntrySelected()) {
893                    startProgrammer(null, re, programmer2);
894                }
895                break;
896            case "comprehensiveprogrammer":
897                if (checkIfEntrySelected()) {
898                    startProgrammer(null, re, programmer1);
899                }
900                break;
901            case "editthrottlelabels":
902                if (checkIfEntrySelected()) {
903                    startProgrammer(null, re, "dp3" + File.separator + "ThrottleLabels");
904                }
905                break;
906            case "editrostermedia":
907                if (checkIfEntrySelected()) {
908                    startProgrammer(null, re, "dp3" + File.separator + "MediaPane");
909                }
910                break;
911            case "hiderosterimage":
912                hideRosterImage();
913                break;
914            case "summarypane":
915                hideSummary();
916                break;
917            case "copyloco":
918                if (checkIfEntrySelected()) {
919                    copyLoco();
920                }
921                break;
922            case "deleteloco":
923                if (checkIfEntrySelected(true)) {
924                    deleteLoco();
925                }
926                break;
927            case "setprogservice":
928                service.setSelected(true);
929                break;
930            case "setprogops":
931                ops.setSelected(true);
932                break;
933            case "setprogedit":
934                edit.setSelected(true);
935                break;
936            case "groupspane":
937                hideGroups();
938                break;
939            case "quit":
940                saveWindowDetails();
941                handleQuit(new WindowEvent(this, frameInstances.size()));
942                break;
943            case "closewindow":
944                closeWindow(null);
945                break;
946            case "newwindow":
947                newWindow();
948                break;
949            case "resettablecolumns":
950                rtable.resetColumnWidths();
951                break;
952            default:
953                log.error("method {} not found", args[0]);
954                break;
955        }
956    }
957
958    JPanel rosterDetails() {
959        JPanel panel = new JPanel();
960        GridBagLayout gbLayout = new GridBagLayout();
961        GridBagConstraints cL = new GridBagConstraints();
962        GridBagConstraints cR = new GridBagConstraints();
963        Dimension minFieldDim = new Dimension(30, 20);
964        cL.gridx = 0;
965        cL.gridy = 0;
966        cL.ipadx = 3;
967        cL.anchor = GridBagConstraints.EAST;
968        cL.insets = new Insets(0, 0, 0, 15);
969        JLabel row0Label = new JLabel(Bundle.getMessage("FieldID") + ":", JLabel.LEFT);
970        gbLayout.setConstraints(row0Label, cL);
971        panel.setLayout(gbLayout);
972        panel.add(row0Label);
973        cR.gridx = 1;
974        cR.gridy = 0;
975        cR.anchor = GridBagConstraints.WEST;
976        id.setMinimumSize(minFieldDim);
977        gbLayout.setConstraints(id, cR);
978        formatTextAreaAsLabel(id);
979        panel.add(id);
980        cL.gridy = 1;
981        JLabel row1Label = new JLabel(Bundle.getMessage("FieldRoadName") + ":", JLabel.LEFT);
982        gbLayout.setConstraints(row1Label, cL);
983        panel.add(row1Label);
984        cR.gridy = 1;
985        roadName.setMinimumSize(minFieldDim);
986        gbLayout.setConstraints(roadName, cR);
987        formatTextAreaAsLabel(roadName);
988        panel.add(roadName);
989        cL.gridy = 2;
990        JLabel row2Label = new JLabel(Bundle.getMessage("FieldRoadNumber") + ":");
991        gbLayout.setConstraints(row2Label, cL);
992        panel.add(row2Label);
993        cR.gridy = 2;
994        roadNumber.setMinimumSize(minFieldDim);
995        gbLayout.setConstraints(roadNumber, cR);
996        formatTextAreaAsLabel(roadNumber);
997        panel.add(roadNumber);
998        cL.gridy = 3;
999        JLabel row3Label = new JLabel(Bundle.getMessage("FieldManufacturer") + ":");
1000        gbLayout.setConstraints(row3Label, cL);
1001        panel.add(row3Label);
1002        cR.gridy = 3;
1003        mfg.setMinimumSize(minFieldDim);
1004        gbLayout.setConstraints(mfg, cR);
1005        formatTextAreaAsLabel(mfg);
1006        panel.add(mfg);
1007        cL.gridy = 4;
1008        JLabel row4Label = new JLabel(Bundle.getMessage("FieldOwner") + ":");
1009        gbLayout.setConstraints(row4Label, cL);
1010        panel.add(row4Label);
1011        cR.gridy = 4;
1012        owner.setMinimumSize(minFieldDim);
1013        gbLayout.setConstraints(owner, cR);
1014        formatTextAreaAsLabel(owner);
1015        panel.add(owner);
1016        cL.gridy = 5;
1017        JLabel row5Label = new JLabel(Bundle.getMessage("FieldModel") + ":");
1018        gbLayout.setConstraints(row5Label, cL);
1019        panel.add(row5Label);
1020        cR.gridy = 5;
1021        model.setMinimumSize(minFieldDim);
1022        gbLayout.setConstraints(model, cR);
1023        formatTextAreaAsLabel(model);
1024        panel.add(model);
1025        cL.gridy = 6;
1026        JLabel row6Label = new JLabel(Bundle.getMessage("FieldDCCAddress") + ":");
1027        gbLayout.setConstraints(row6Label, cL);
1028        panel.add(row6Label);
1029        cR.gridy = 6;
1030        dccAddress.setMinimumSize(minFieldDim);
1031        gbLayout.setConstraints(dccAddress, cR);
1032        formatTextAreaAsLabel(dccAddress);
1033        panel.add(dccAddress);
1034        cL.gridy = 7;
1035        cR.gridy = 7;
1036        cL.gridy = 8;
1037        cR.gridy = 8;
1038        cL.gridy = 9;
1039        JLabel row9Label = new JLabel(Bundle.getMessage("FieldDecoderFamily") + ":");
1040        gbLayout.setConstraints(row9Label, cL);
1041        panel.add(row9Label);
1042        cR.gridy = 9;
1043        decoderFamily.setMinimumSize(minFieldDim);
1044        gbLayout.setConstraints(decoderFamily, cR);
1045        formatTextAreaAsLabel(decoderFamily);
1046        panel.add(decoderFamily);
1047        cL.gridy = 10;
1048        JLabel row10Label = new JLabel(Bundle.getMessage("FieldDecoderModel") + ":");
1049        gbLayout.setConstraints(row10Label, cL);
1050        panel.add(row10Label);
1051        cR.gridy = 10;
1052        decoderModel.setMinimumSize(minFieldDim);
1053        gbLayout.setConstraints(decoderModel, cR);
1054        formatTextAreaAsLabel(decoderModel);
1055        panel.add(decoderModel);
1056        cL.gridy = 11;
1057        cR.gridy = 11;
1058        cL.gridy = 12;
1059        JLabel row12Label = new JLabel(Bundle.getMessage("FieldFilename") + ":");
1060        gbLayout.setConstraints(row12Label, cL);
1061        panel.add(row12Label);
1062        cR.gridy = 12;
1063        filename.setMinimumSize(minFieldDim);
1064        gbLayout.setConstraints(filename, cR);
1065        formatTextAreaAsLabel(filename);
1066        panel.add(filename);
1067        cL.gridy = 13;
1068        /*
1069         * JLabel row13Label = new
1070         * JLabel(Bundle.getMessage("FieldDateUpdated")+":");
1071         * gbLayout.setConstraints(row13Label,cL); panel.add(row13Label);
1072         */
1073        cR.gridy = 13;
1074        /*
1075         * filename.setMinimumSize(minFieldDim);
1076         * gbLayout.setConstraints(dateUpdated,cR); panel.add(dateUpdated);
1077         */
1078        formatTextAreaAsLabel(dateUpdated);
1079        JPanel retval = new JPanel(new FlowLayout(FlowLayout.LEFT));
1080        retval.add(panel);
1081        return retval;
1082    }
1083
1084    void saveWindowDetails() {
1085        prefsMgr.setSimplePreferenceState(this.getClass().getName() + ".hideSummary", hideBottomPane);
1086        prefsMgr.setSimplePreferenceState(this.getClass().getName() + ".hideGroups", hideGroups);
1087        prefsMgr.setSimplePreferenceState(this.getClass().getName() + ".hideRosterImage", hideRosterImage);
1088        prefsMgr.setProperty(getWindowFrameRef(), SELECTED_ROSTER_GROUP, groups.getSelectedRosterGroup());
1089        String selectedProgMode = "edit";
1090        if (service.isSelected()) {
1091            selectedProgMode = "service";
1092        }
1093        if (ops.isSelected()) {
1094            selectedProgMode = "ops";
1095        }
1096        prefsMgr.setProperty(getWindowFrameRef(), "selectedProgrammer", selectedProgMode);
1097
1098        if (rosterGroupSplitPane.getDividerLocation() > 2) {
1099            prefsMgr.setProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation", rosterGroupSplitPane.getDividerLocation());
1100        } else if (groupSplitPaneLocation > 2) {
1101            prefsMgr.setProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation", groupSplitPaneLocation);
1102        }
1103    }
1104
1105    /**
1106     * Identify locomotive complete, act on it by setting the GUI. This will
1107     * fire "GUI changed" events which will reset the decoder GUI.
1108     *
1109     * @param dccAddress address of locomotive
1110     * @param isLong     true if address is long; false if short
1111     * @param mfgId      manufacturer id as in decoder
1112     * @param modelId    model id as in decoder
1113     */
1114    protected void selectLoco(int dccAddress, boolean isLong, int mfgId, int modelId) {
1115        // raise the button again
1116        // idloco.setSelected(false);
1117        // locate that loco
1118        inStartProgrammer = false;
1119        if (re != null) {
1120            //We remove the propertychangelistener if we had a previoulsy selected entry;
1121            re.removePropertyChangeListener(rosterEntryUpdateListener);
1122        }
1123        List<RosterEntry> l = Roster.getDefault().matchingList(null, null, Integer.toString(dccAddress), null, null, null, null);
1124        log.debug("selectLoco found {} matches", l.size());
1125        if (!l.isEmpty()) {
1126            if (l.size() > 1) {
1127                //More than one possible loco, so check long flag
1128                List<RosterEntry> l2 = new ArrayList<>();
1129                for (RosterEntry _re : l) {
1130                    if (_re.isLongAddress() == isLong) {
1131                        l2.add(_re);
1132                    }
1133                }
1134                if (l2.size() == 1) {
1135                    re = l2.get(0);
1136                } else {
1137                    if (l2.isEmpty()) {
1138                        l2 = l;
1139                    }
1140                    // Still more than one possible loco, so check against the decoder family
1141                    log.trace("Checking against decoder family with mfg {} model {}", mfgId, modelId);
1142                    List<RosterEntry> l3 = new ArrayList<>();
1143                    List<DecoderFile> temp = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, null, "" + mfgId, "" + modelId, null, null);
1144                    log.trace("found {}", temp.size());
1145                    ArrayList<String> decoderFam = new ArrayList<>();
1146                    for (DecoderFile f : temp) {
1147                        if (!decoderFam.contains(f.getModel())) {
1148                            decoderFam.add(f.getModel());
1149                        }
1150                    }
1151                    log.trace("matched {} times", decoderFam.size());
1152
1153                    for (RosterEntry _re : l2) {
1154                        if (decoderFam.contains(_re.getDecoderModel())) {
1155                            l3.add(_re);
1156                        }
1157                    }
1158                    if (l3.isEmpty()) {
1159                        //Unable to determine the loco against the manufacture therefore will be unable to further identify against decoder.
1160                        re = l2.get(0);
1161                    } else {
1162                        //We have no other options to match against so will return the first one we come across;
1163                        re = l3.get(0);
1164                    }
1165                }
1166            } else {
1167                re = l.get(0);
1168            }
1169            re.addPropertyChangeListener(rosterEntryUpdateListener);
1170            rtable.setSelection(re);
1171            updateDetails();
1172            rtable.moveTableViewToSelected();
1173        } else {
1174            log.warn("Read address {}, but no such loco in roster", dccAddress); //"No roster entry found; changed to promote the number to the front, June 2022,  Bill Chown"
1175            JmriJOptionPane.showMessageDialog(this, dccAddress + " was read from the decoder\nbut has not been found in the Roster", dccAddress + " No roster entry found", JmriJOptionPane.INFORMATION_MESSAGE);
1176        }
1177    }
1178
1179    /**
1180     * Simple method to change over the programmer buttons.
1181     * <p>
1182     * TODO This should be implemented with the buttons in their own class etc.
1183     * but this will work for now.
1184     *
1185     * @param buttonId   1 or 2; use 1 for basic programmer; 2 for comprehensive
1186     *                   programmer
1187     * @param programmer name of programmer
1188     * @param buttonText button title
1189     */
1190    public void setProgrammerLaunch(int buttonId, String programmer, String buttonText) {
1191        if (buttonId == 1) {
1192            programmer1 = programmer;
1193            prog1Button.setText(buttonText);
1194        } else if (buttonId == 2) {
1195            programmer2 = programmer;
1196            prog2Button.setText(buttonText);
1197        }
1198    }
1199
1200    public void setSelectedRosterGroup(String rosterGroup) {
1201        groups.setSelectedRosterGroup(rosterGroup);
1202    }
1203
1204    protected void showPopup(JmriMouseEvent e) {
1205        int row = rtable.getTable().rowAtPoint(e.getPoint());
1206        if (!rtable.getTable().isRowSelected(row)) {
1207            rtable.getTable().changeSelection(row, 0, false, false);
1208        }
1209        JPopupMenu popupMenu = new JPopupMenu();
1210
1211        JMenuItem menuItem = new JMenuItem(Bundle.getMessage("Program"));
1212        menuItem.addActionListener((ActionEvent e1) -> startProgrammer(null, re, programmer1));
1213        if (re == null) {
1214            menuItem.setEnabled(false);
1215        }
1216        popupMenu.add(menuItem);
1217        ButtonGroup group = new ButtonGroup();
1218        group.add(contextService);
1219        group.add(contextOps);
1220        group.add(contextEdit);
1221        JMenu progMenu = new JMenu(Bundle.getMessage("ProgrammerType"));
1222        contextService.addActionListener((ActionEvent e1) -> {
1223            service.setSelected(true);
1224            updateProgMode();
1225        });
1226        progMenu.add(contextService);
1227        contextOps.addActionListener((ActionEvent e1) -> {
1228            ops.setSelected(true);
1229            updateProgMode();
1230        });
1231        progMenu.add(contextOps);
1232        contextEdit.addActionListener((ActionEvent e1) -> {
1233            edit.setSelected(true);
1234            updateProgMode();
1235        });
1236        if (service.isSelected()) {
1237            contextService.setSelected(true);
1238        } else if (ops.isSelected()) {
1239            contextOps.setSelected(true);
1240        } else {
1241            contextEdit.setSelected(true);
1242        }
1243        progMenu.add(contextEdit);
1244        popupMenu.add(progMenu);
1245
1246        popupMenu.addSeparator();
1247        menuItem = new JMenuItem(Bundle.getMessage("LabelsAndMedia"));
1248        menuItem.addActionListener((ActionEvent e1) -> editMediaButton());
1249        if (re == null) {
1250            menuItem.setEnabled(false);
1251        }
1252        popupMenu.add(menuItem);
1253        menuItem = new JMenuItem(Bundle.getMessage("Throttle"));
1254        menuItem.addActionListener((ActionEvent e1) -> {
1255            ThrottleFrame tf = InstanceManager.getDefault(ThrottleFrameManager.class).createThrottleFrame();
1256            tf.toFront();
1257            tf.getAddressPanel().getRosterEntrySelector().setSelectedRosterGroup(getSelectedRosterGroup());
1258            tf.getAddressPanel().setRosterEntry(re);
1259        });
1260        if (re == null) {
1261            menuItem.setEnabled(false);
1262        }
1263        popupMenu.add(menuItem);
1264        popupMenu.addSeparator();
1265
1266        menuItem = new JMenuItem(Bundle.getMessage("PrintSelection"));
1267        menuItem.addActionListener((ActionEvent e1) -> printLoco(false));
1268        if (re == null) {
1269            menuItem.setEnabled(false);
1270        }
1271        popupMenu.add(menuItem);
1272        menuItem = new JMenuItem(Bundle.getMessage("PreviewSelection"));
1273        menuItem.addActionListener((ActionEvent e1) -> printLoco(true));
1274        if (re == null) {
1275            menuItem.setEnabled(false);
1276        }
1277        popupMenu.add(menuItem);
1278        popupMenu.addSeparator();
1279
1280        menuItem = new JMenuItem(Bundle.getMessage("Duplicateddd"));
1281        menuItem.addActionListener((ActionEvent e1) -> copyLoco());
1282        if (re == null) {
1283            menuItem.setEnabled(false);
1284        }
1285        popupMenu.add(menuItem);
1286        menuItem = new JMenuItem(this.getSelectedRosterGroup() != null ? Bundle.getMessage("DeleteFromGroup") : Bundle.getMessage("DeleteFromRoster")); // NOI18N
1287        menuItem.addActionListener((ActionEvent e1) -> deleteLoco());
1288        popupMenu.add(menuItem);
1289        menuItem.setEnabled(this.getSelectedRosterEntries().length > 0);
1290
1291        popupMenu.show(e.getComponent(), e.getX(), e.getY());
1292    }
1293
1294    /**
1295     * Start the identify operation after [Identify Loco] button pressed.
1296     * <p>
1297     * This defines what happens when Identify is done.
1298     */
1299    //taken out of CombinedLocoSelPane
1300    protected void startIdentifyLoco() {
1301        final RosterFrame me = this;
1302        Programmer programmer = null;
1303        if (modePanel.isSelected()) {
1304            programmer = modePanel.getProgrammer();
1305        }
1306        if (programmer == null) {
1307            GlobalProgrammerManager gpm = InstanceManager.getNullableDefault(GlobalProgrammerManager.class);
1308            if (gpm != null) {
1309                programmer = gpm.getGlobalProgrammer();
1310                log.warn("Selector did not provide a programmer, attempt to use GlobalProgrammerManager default: {}", programmer);
1311            } else {
1312                log.warn("Selector did not provide a programmer, and no ProgramManager found in InstanceManager");
1313            }
1314        }
1315
1316        // if failed to get programmer, tell user and stop
1317        if (programmer == null) {
1318            log.error("Identify loco called when no service mode programmer is available; button should have been disabled");
1319            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("IdentifyError"));
1320            return;
1321        }
1322
1323        // and now do the work
1324        IdentifyLoco ident = new IdentifyLoco(programmer) {
1325            private final RosterFrame who = me;
1326
1327            @Override
1328            protected void done(int dccAddress) {
1329                // if Done, updated the selected decoder
1330                // on the GUI thread, right now
1331                jmri.util.ThreadingUtil.runOnGUI(() -> who.selectLoco(dccAddress, !shortAddr, cv8val, cv7val));
1332            }
1333
1334            @Override
1335            protected void message(String m) {
1336                // on the GUI thread, right now
1337                jmri.util.ThreadingUtil.runOnGUI(() -> statusField.setText(m));
1338            }
1339
1340            @Override
1341            protected void error() {
1342                // raise the button again
1343                //idloco.setSelected(false);
1344            }
1345        };
1346        ident.start();
1347    }
1348
1349    protected void startProgrammer(DecoderFile decoderFile, RosterEntry re, String filename) {
1350        if (inStartProgrammer) {
1351            log.debug("Call to start programmer has been called twice when the first call hasn't opened");
1352            return;
1353        }
1354        if (!checkIfEntrySelected()) {
1355            return;
1356        }
1357        try {
1358            setCursor(new Cursor(Cursor.WAIT_CURSOR));
1359            inStartProgrammer = true;
1360            String title = re.getId();
1361            JFrame progFrame = null;
1362            if (edit.isSelected()) {
1363                progFrame = new PaneProgFrame(decoderFile, re, title, "programmers" + File.separator + filename + ".xml", null, false) {
1364                    @Override
1365                    protected JPanel getModePane() {
1366                        return null;
1367                    } // hide prog mode buttons pane
1368                };
1369            } else if (service.isSelected()) {
1370                progFrame = new PaneServiceProgFrame(decoderFile, re, title, "programmers" + File.separator + filename + ".xml", modePanel.getProgrammer());
1371            } else if (ops.isSelected()) {
1372                int address = Integer.parseInt(re.getDccAddress());
1373                boolean longAddr = re.isLongAddress();
1374                Programmer pProg = InstanceManager.getDefault(AddressedProgrammerManager.class).getAddressedProgrammer(longAddr, address);
1375                progFrame = new PaneOpsProgFrame(decoderFile, re, title, "programmers" + File.separator + filename + ".xml", pProg);
1376            }
1377            if (progFrame == null) {
1378                return;
1379            }
1380            progFrame.pack();
1381            progFrame.setVisible(true);
1382        } finally {
1383            setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
1384        }
1385        inStartProgrammer = false;
1386    }
1387
1388    /**
1389     * Create and display a status bar along the bottom edge of the Roster main
1390     * pane.
1391     * <p>
1392     * TODO This status bar needs sorting out properly
1393     */
1394    protected void statusBar() {
1395        addToStatusBox(serviceModeProgrammerLabel, null);
1396        addToStatusBox(operationsModeProgrammerLabel, null);
1397        JLabel programmerStatusLabel = new JLabel(Bundle.getMessage("ProgrammerStatus"));
1398        statusField.setText(Bundle.getMessage("StateIdle"));
1399        addToStatusBox(programmerStatusLabel, statusField);
1400        Profile profile = ProfileManager.getDefault().getActiveProfile();
1401        if (profile != null) {
1402            addToStatusBox(new JLabel(Bundle.getMessage("ActiveProfile", profile.getName())), null);
1403        }
1404    }
1405
1406    protected void systemsMenu() {
1407        ActiveSystemsMenu.addItems(getMenu());
1408        getMenu().add(new WindowMenu(this));
1409    }
1410
1411    void updateDetails() {
1412        if (re == null) {
1413            String value = (rtable.getTable().getSelectedRowCount() > 1) ? "Multiple Items Selected" : "";
1414            filename.setText(value);
1415            dateUpdated.setText(value);
1416            decoderModel.setText(value);
1417            decoderFamily.setText(value);
1418            id.setText(value);
1419            roadName.setText(value);
1420            dccAddress.setText(value);
1421            roadNumber.setText(value);
1422            mfg.setText(value);
1423            model.setText(value);
1424            owner.setText(value);
1425            locoImage.setImagePath(null);
1426        } else {
1427            filename.setText(re.getFileName());
1428            dateUpdated.setText((re.getDateModified() != null)
1429                    ? DateFormat.getDateTimeInstance().format(re.getDateModified())
1430                    : re.getDateUpdated());
1431            decoderModel.setText(re.getDecoderModel());
1432            decoderFamily.setText(re.getDecoderFamily());
1433            dccAddress.setText(re.getDccAddress());
1434            id.setText(re.getId());
1435            roadName.setText(re.getRoadName());
1436            roadNumber.setText(re.getRoadNumber());
1437            mfg.setText(re.getMfg());
1438            model.setText(re.getModel());
1439            owner.setText(re.getOwner());
1440            locoImage.setImagePath(re.getImagePath());
1441            if (hideRosterImage) {
1442                locoImage.setVisible(false);
1443            } else {
1444                locoImage.setVisible(true);
1445            }
1446            prog1Button.setEnabled(true);
1447            prog2Button.setEnabled(true);
1448            throttleLabels.setEnabled(true);
1449            rosterMedia.setEnabled(true);
1450            throttleLaunch.setEnabled(true);
1451            updateProgMode();
1452        }
1453    }
1454
1455    void updateProgMode() {
1456        String progMode;
1457        if (service.isSelected()) {
1458            progMode = "setprogservice";
1459        } else if (ops.isSelected()) {
1460            progMode = "setprogops";
1461        } else {
1462            progMode = "setprogedit";
1463        }
1464        firePropertyChange(progMode, "setSelected", true);
1465    }
1466
1467    /**
1468     * Handle setting up and updating the GUI for the types of programmer
1469     * available.
1470     *
1471     * @param evt the triggering event; if not null and if a removal of a
1472     *            ProgrammerManager, care will be taken not to trigger the
1473     *            automatic creation of a new ProgrammerManager
1474     */
1475    protected void updateProgrammerStatus(@CheckForNull PropertyChangeEvent evt) {
1476        log.debug("Updating Programmer Status");
1477        ConnectionConfig oldServMode = serModeProCon;
1478        ConnectionConfig oldOpsMode = opsModeProCon;
1479        GlobalProgrammerManager gpm = null;
1480        AddressedProgrammerManager apm = null;
1481
1482        // Find the connection that goes with the global programmer
1483        // test that IM has a default GPM, or that event is not the removal of a GPM
1484        if (InstanceManager.containsDefault(GlobalProgrammerManager.class)
1485                || (evt != null
1486                && evt.getPropertyName().equals(InstanceManager.getDefaultsPropertyName(GlobalProgrammerManager.class))
1487                && evt.getNewValue() == null)) {
1488            gpm = InstanceManager.getNullableDefault(GlobalProgrammerManager.class);
1489            log.trace("found global programming manager {}", gpm);
1490        }
1491        if (gpm != null) {
1492            String serviceModeProgrammerName = gpm.getUserName();
1493            log.debug("GlobalProgrammerManager found of class {} name {} ", gpm.getClass(), serviceModeProgrammerName);
1494            InstanceManager.getOptionalDefault(ConnectionConfigManager.class).ifPresent((ccm) -> {
1495                for (ConnectionConfig connection : ccm) {
1496                    log.debug("Checking connection name {}", connection.getConnectionName());
1497                    if (connection.getConnectionName() != null && connection.getConnectionName().equals(serviceModeProgrammerName)) {
1498                        log.debug("Connection found for GlobalProgrammermanager");
1499                        serModeProCon = connection;
1500                    }
1501                }
1502            });
1503        }
1504
1505        // Find the connection that goes with the addressed programmer
1506        // test that IM has a default APM, or that event is not the removal of an APM
1507        if (InstanceManager.containsDefault(AddressedProgrammerManager.class)
1508                || (evt != null
1509                && evt.getPropertyName().equals(InstanceManager.getDefaultsPropertyName(AddressedProgrammerManager.class))
1510                && evt.getNewValue() == null)) {
1511            apm = InstanceManager.getNullableDefault(AddressedProgrammerManager.class);
1512            log.trace("found addressed programming manager {}", gpm);
1513        }
1514        if (apm != null) {
1515            String opsModeProgrammerName = apm.getUserName();
1516            log.debug("AddressedProgrammerManager found of class {} name {} ", apm.getClass(), opsModeProgrammerName);
1517            InstanceManager.getOptionalDefault(ConnectionConfigManager.class).ifPresent((ccm) -> {
1518                for (ConnectionConfig connection : ccm) {
1519                    log.debug("Checking connection name {}", connection.getConnectionName());
1520                    if (connection.getConnectionName() != null && connection.getConnectionName().equals(opsModeProgrammerName)) {
1521                        log.debug("Connection found for AddressedProgrammermanager");
1522                        opsModeProCon = connection;
1523                    }
1524                }
1525            });
1526        }
1527
1528        log.trace("start global check with {}, {}, {}", serModeProCon, gpm, (gpm != null ? gpm.isGlobalProgrammerAvailable() : "<none>"));
1529        if (serModeProCon != null && gpm != null && gpm.isGlobalProgrammerAvailable()) {
1530            if (ConnectionStatus.instance().isConnectionOk(serModeProCon.getConnectionName(), serModeProCon.getInfo())) {
1531                log.debug("GPM Connection online 1");
1532                serviceModeProgrammerLabel.setText(
1533                        Bundle.getMessage("ServiceModeProgOnline", serModeProCon.getConnectionName()));
1534                serviceModeProgrammerLabel.setForeground(new Color(0, 128, 0));
1535            } else {
1536                log.debug("GPM Connection offline");
1537                serviceModeProgrammerLabel.setText(
1538                        Bundle.getMessage("ServiceModeProgOffline", serModeProCon.getConnectionName()));
1539                serviceModeProgrammerLabel.setForeground(Color.red);
1540            }
1541            if (oldServMode == null) {
1542                log.debug("Re-enable user interface");
1543                contextService.setEnabled(true);
1544                contextService.setVisible(true);
1545                service.setEnabled(true);
1546                service.setVisible(true);
1547                firePropertyChange("setprogservice", "setEnabled", true);
1548                getToolBar().getComponents()[1].setEnabled(true);
1549            }
1550        } else if (gpm != null && gpm.isGlobalProgrammerAvailable()) {
1551            if (ConnectionStatus.instance().isSystemOk(gpm.getUserName())) {
1552                log.debug("GPM Connection online 2");
1553                serviceModeProgrammerLabel.setText(
1554                        Bundle.getMessage("ServiceModeProgOnline", gpm.getUserName()));
1555                serviceModeProgrammerLabel.setForeground(new Color(0, 128, 0));
1556            } else {
1557                log.debug("GPM Connection onffline");
1558                serviceModeProgrammerLabel.setText(
1559                        Bundle.getMessage("ServiceModeProgOffline", gpm.getUserName()));
1560                serviceModeProgrammerLabel.setForeground(Color.red);
1561            }
1562            if (oldServMode == null) {
1563                log.debug("Re-enable user interface");
1564                contextService.setEnabled(true);
1565                contextService.setVisible(true);
1566                service.setEnabled(true);
1567                service.setVisible(true);
1568                firePropertyChange("setprogservice", "setEnabled", true);
1569                getToolBar().getComponents()[1].setEnabled(true);
1570            }
1571        } else {
1572            // No service programmer available, disable interface sections not available
1573            log.debug("no service programmer");
1574            serviceModeProgrammerLabel.setText(Bundle.getMessage("NoServiceProgrammerAvailable"));
1575            serviceModeProgrammerLabel.setForeground(Color.red);
1576            if (oldServMode != null) {
1577                contextService.setEnabled(false);
1578                contextService.setVisible(false);
1579                service.setEnabled(false);
1580                service.setVisible(false);
1581                firePropertyChange("setprogservice", "setEnabled", false);
1582            }
1583            // Disable Identify in toolBar
1584            // This relies on it being the 2nd item in the toolbar, as defined in xml//config/parts/jmri/jmrit/roster/swing/RosterFrameToolBar.xml
1585            // Because of I18N, we don't look for a particular Action name here
1586            getToolBar().getComponents()[1].setEnabled(false);
1587            serModeProCon = null;
1588        }
1589
1590        if (opsModeProCon != null && apm != null && apm.isAddressedModePossible()) {
1591            if (ConnectionStatus.instance().isConnectionOk(opsModeProCon.getConnectionName(), opsModeProCon.getInfo())) {
1592                log.debug("Ops Mode Connection online");
1593                operationsModeProgrammerLabel.setText(
1594                        Bundle.getMessage("OpsModeProgOnline", opsModeProCon.getConnectionName()));
1595                operationsModeProgrammerLabel.setForeground(new Color(0, 128, 0));
1596            } else {
1597                log.debug("Ops Mode Connection offline");
1598                operationsModeProgrammerLabel.setText(
1599                        Bundle.getMessage("OpsModeProgOffline", opsModeProCon.getConnectionName()));
1600                operationsModeProgrammerLabel.setForeground(Color.red);
1601            }
1602            if (oldOpsMode == null) {
1603                contextOps.setEnabled(true);
1604                contextOps.setVisible(true);
1605                ops.setEnabled(true);
1606                ops.setVisible(true);
1607                firePropertyChange("setprogops", "setEnabled", true);
1608            }
1609        } else if (apm != null && apm.isAddressedModePossible()) {
1610            if (ConnectionStatus.instance().isSystemOk(apm.getUserName())) {
1611                log.debug("Ops Mode Connection online");
1612                operationsModeProgrammerLabel.setText(
1613                        Bundle.getMessage("OpsModeProgOnline", apm.getUserName()));
1614                operationsModeProgrammerLabel.setForeground(new Color(0, 128, 0));
1615            } else {
1616                log.debug("Ops Mode Connection offline");
1617                operationsModeProgrammerLabel.setText(
1618                        Bundle.getMessage("OpsModeProgOffline", apm.getUserName()));
1619                operationsModeProgrammerLabel.setForeground(Color.red);
1620            }
1621            if (oldOpsMode == null) {
1622                contextOps.setEnabled(true);
1623                contextOps.setVisible(true);
1624                ops.setEnabled(true);
1625                ops.setVisible(true);
1626                firePropertyChange("setprogops", "setEnabled", true);
1627            }
1628        } else {
1629            // No ops mode programmer available, disable interface sections not available
1630            log.debug("no ops mode programmer");
1631            operationsModeProgrammerLabel.setText(Bundle.getMessage("NoOpsProgrammerAvailable"));
1632            operationsModeProgrammerLabel.setForeground(Color.red);
1633            if (oldOpsMode != null) {
1634                contextOps.setEnabled(false);
1635                contextOps.setVisible(false);
1636                ops.setEnabled(false);
1637                ops.setVisible(false);
1638                firePropertyChange("setprogops", "setEnabled", false);
1639            }
1640            opsModeProCon = null;
1641        }
1642        String strProgMode;
1643        if (service.isEnabled()) {
1644            contextService.setSelected(true);
1645            service.setSelected(true);
1646            strProgMode = "setprogservice";
1647            modePanel.setVisible(true);
1648        } else if (ops.isEnabled()) {
1649            contextOps.setSelected(true);
1650            ops.setSelected(true);
1651            strProgMode = "setprogops";
1652            modePanel.setVisible(false);
1653        } else {
1654            contextEdit.setSelected(true);
1655            edit.setSelected(true);
1656            modePanel.setVisible(false);
1657            strProgMode = "setprogedit";
1658        }
1659        firePropertyChange(strProgMode, "setSelected", true);
1660    }
1661
1662    @Override
1663    public void windowClosing(WindowEvent e) {
1664        closeWindow(e);
1665    }
1666
1667    /**
1668     * Displays a context (right-click) menu for a roster entry.
1669     */
1670    private class RosterPopupListener extends JmriMouseAdapter {
1671
1672        @Override
1673        public void mousePressed(JmriMouseEvent e) {
1674            if (e.isPopupTrigger()) {
1675                showPopup(e);
1676            }
1677        }
1678
1679        @Override
1680        public void mouseReleased(JmriMouseEvent e) {
1681            if (e.isPopupTrigger()) {
1682                showPopup(e);
1683            }
1684        }
1685
1686        @Override
1687        public void mouseClicked(JmriMouseEvent e) {
1688            if (e.isPopupTrigger()) {
1689                showPopup(e);
1690                return;
1691            }
1692            if (e.getClickCount() == 2) {
1693                startProgrammer(null, re, programmer1);
1694            }
1695        }
1696    }
1697
1698    private static class ExportRosterItem extends ExportRosterItemAction {
1699
1700        ExportRosterItem(String pName, Component pWho, RosterEntry re) {
1701            super(pName, pWho);
1702            super.setExistingEntry(re);
1703        }
1704
1705        @Override
1706        protected boolean selectFrom() {
1707            return true;
1708        }
1709    }
1710
1711    private static class CopyRosterItem extends CopyRosterItemAction {
1712
1713        CopyRosterItem(String pName, Component pWho, RosterEntry re) {
1714            super(pName, pWho);
1715            super.setExistingEntry(re);
1716        }
1717
1718        @Override
1719        protected boolean selectFrom() {
1720            return true;
1721        }
1722    }
1723    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterFrame.class);
1724
1725}