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