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