001package jmri.jmrit.roster.swing;
002
003import java.awt.BorderLayout;
004import java.awt.Color;
005import java.awt.Component;
006import java.awt.Cursor;
007import java.awt.Dimension;
008import java.awt.FlowLayout;
009import java.awt.GridBagConstraints;
010import java.awt.GridBagLayout;
011import java.awt.GridLayout;
012import java.awt.Insets;
013import java.awt.datatransfer.Transferable;
014import java.awt.event.ActionEvent;
015import java.awt.event.ActionListener;
016import java.awt.event.WindowEvent;
017import java.awt.image.BufferedImage;
018
019import java.beans.PropertyChangeEvent;
020import java.beans.PropertyChangeListener;
021import java.io.File;
022import java.io.IOException;
023import java.text.DateFormat;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.List;
027
028import javax.annotation.CheckForNull;
029import javax.imageio.ImageIO;
030import javax.swing.*;
031import javax.swing.event.ListSelectionEvent;
032
033import jmri.AddressedProgrammerManager;
034import jmri.GlobalProgrammerManager;
035import jmri.InstanceManager;
036import jmri.Programmer;
037import jmri.ShutDownManager;
038import jmri.UserPreferencesManager;
039import jmri.jmrit.decoderdefn.DecoderFile;
040import jmri.jmrit.decoderdefn.DecoderIndexFile;
041import jmri.jmrit.progsupport.ProgModeSelector;
042import jmri.jmrit.progsupport.ProgServiceModeComboBox;
043import jmri.jmrit.roster.CopyRosterItemAction;
044import jmri.jmrit.roster.DeleteRosterItemAction;
045import jmri.jmrit.roster.ExportRosterItemAction;
046import jmri.jmrit.roster.IdentifyLoco;
047import jmri.jmrit.roster.PrintRosterEntry;
048import jmri.jmrit.roster.Roster;
049import jmri.jmrit.roster.RosterEntry;
050import jmri.jmrit.roster.RosterEntrySelector;
051import jmri.jmrit.roster.rostergroup.RosterGroupSelector;
052import jmri.jmrit.symbolicprog.ProgrammerConfigManager;
053import jmri.jmrit.symbolicprog.tabbedframe.PaneOpsProgFrame;
054import jmri.jmrit.symbolicprog.tabbedframe.PaneProgFrame;
055import jmri.jmrit.symbolicprog.tabbedframe.PaneServiceProgFrame;
056import jmri.jmrit.throttle.LargePowerManagerButton;
057import jmri.jmrit.throttle.ThrottleFrame;
058import jmri.jmrit.throttle.ThrottleFrameManager;
059import jmri.jmrix.ActiveSystemsMenu;
060import jmri.jmrix.ConnectionConfig;
061import jmri.jmrix.ConnectionConfigManager;
062import jmri.jmrix.ConnectionStatus;
063import jmri.profile.Profile;
064import jmri.profile.ProfileManager;
065import jmri.swing.JTablePersistenceManager;
066import jmri.swing.RowSorterUtil;
067import jmri.util.FileUtil;
068import jmri.util.HelpUtil;
069import jmri.util.WindowMenu;
070import jmri.util.datatransfer.RosterEntrySelection;
071import jmri.util.swing.JmriAbstractAction;
072import jmri.util.swing.JmriJOptionPane;
073import jmri.util.swing.JmriMouseAdapter;
074import jmri.util.swing.JmriMouseEvent;
075import jmri.util.swing.JmriMouseListener;
076import jmri.util.swing.ResizableImagePanel;
077import jmri.util.swing.WindowInterface;
078import jmri.util.swing.multipane.TwoPaneTBWindow;
079
080/**
081 * A window for Roster management.
082 * <p>
083 * TODO: Several methods are copied from PaneProgFrame and should be refactored
084 * No programmer support yet (dummy object below). Color only covering borders.
085 * No reset toolbar support yet. No glass pane support (See DecoderPro3Panes
086 * class and usage below). Special panes (Roster entry, attributes, graphics)
087 * not included. How do you pick a programmer file? (hardcoded) Initialization
088 * needs partial deferral, too for 1st pane to appear.
089 *
090 * @see jmri.jmrit.symbolicprog.tabbedframe.PaneSet
091 *
092 * @author Bob Jacobsen Copyright (C) 2010, 2016
093 * @author Kevin Dickerson Copyright (C) 2011
094 * @author Randall Wood Copyright (C) 2012
095 */
096public class RosterFrame extends TwoPaneTBWindow implements RosterEntrySelector, RosterGroupSelector {
097
098    static final ArrayList<RosterFrame> frameInstances = new ArrayList<>();
099    protected boolean allowQuit = true;
100    protected String baseTitle = "Roster";
101    protected JmriAbstractAction newWindowAction;
102
103    public RosterFrame() {
104        this(Bundle.getMessage("RosterTitle"));
105    }
106
107    public RosterFrame(String name) {
108        this(name,
109                "xml/config/parts/jmri/jmrit/roster/swing/RosterFrameMenu.xml",
110                "xml/config/parts/jmri/jmrit/roster/swing/RosterFrameToolBar.xml");
111    }
112
113    public RosterFrame(String name, String menubarFile, String toolbarFile) {
114        super(name, menubarFile, toolbarFile);
115        this.allowInFrameServlet = false;
116        this.setBaseTitle(name);
117        this.buildWindow();
118        this.locoSelected(null);
119    }
120
121    final JRadioButtonMenuItem contextEdit = new JRadioButtonMenuItem(Bundle.getMessage("EditOnly"));
122    final JRadioButtonMenuItem contextOps = new JRadioButtonMenuItem(Bundle.getMessage("ProgrammingOnMain"));
123    final JRadioButtonMenuItem contextService = new JRadioButtonMenuItem(Bundle.getMessage("ProgrammingTrack"));
124    final JTextPane dateUpdated = new JTextPane();
125    final JTextPane dccAddress = new JTextPane();
126    final JTextPane decoderFamily = new JTextPane();
127    final JTextPane decoderModel = new JTextPane();
128    final JRadioButton edit = new JRadioButton(Bundle.getMessage("EditOnly"));
129    final JTextPane filename = new JTextPane();
130    JLabel firstHelpLabel;
131    //int firstTimeAddedEntry = 0x00;
132    int groupSplitPaneLocation = 0;
133    RosterGroupsPanel groups;
134    boolean hideGroups = false;
135    boolean hideRosterImage = false;
136    final JTextPane id = new JTextPane();
137    boolean inStartProgrammer = false;
138    ResizableImagePanel locoImage;
139    JTextPane maxSpeed = new JTextPane();
140    final JTextPane mfg = new JTextPane();
141    final ProgModeSelector modePanel = new ProgServiceModeComboBox();
142    final JTextPane model = new JTextPane();
143    final JLabel operationsModeProgrammerLabel = new JLabel();
144    final JRadioButton ops = new JRadioButton(Bundle.getMessage("ProgrammingOnMain"));
145    ConnectionConfig opsModeProCon = null;
146    final JTextPane owner = new JTextPane();
147    UserPreferencesManager prefsMgr;
148    final JButton prog1Button = new JButton(Bundle.getMessage("Program"));
149    final JButton prog2Button = new JButton(Bundle.getMessage("BasicProgrammer"));
150    ActionListener programModeListener;
151
152    // These are the names of the programmer _files_, not what should be displayed to the user
153    String programmer1 = "Comprehensive"; // NOI18N
154    String programmer2 = "Basic"; // NOI18N
155
156    final java.util.ResourceBundle rb = java.util.ResourceBundle.getBundle("apps.AppsBundle");
157    //current selected loco
158    transient RosterEntry re;
159    final JTextPane roadName = new JTextPane();
160    final JTextPane roadNumber = new JTextPane();
161    final JPanel rosterDetailPanel = new JPanel();
162    PropertyChangeListener rosterEntryUpdateListener;
163    JSplitPane rosterGroupSplitPane;
164    final JButton rosterMedia = new JButton(Bundle.getMessage("LabelsAndMedia"));
165    RosterTable rtable;
166    ConnectionConfig serModeProCon = null;
167    final JRadioButton service = new JRadioButton(Bundle.getMessage("ProgrammingTrack"));
168    final JLabel serviceModeProgrammerLabel = new JLabel();
169    final JLabel statusField = new JLabel();
170    final Dimension summaryPaneDim = new Dimension(0, 170);
171    final JButton throttleLabels = new JButton(Bundle.getMessage("ThrottleLabels"));
172    final JButton throttleLaunch = new JButton(Bundle.getMessage("Throttle"));
173
174    protected void additionsToToolBar() {
175        getToolBar().add(new LargePowerManagerButton(true));
176        getToolBar().add(Box.createHorizontalGlue());
177        JPanel p = new JPanel();
178        p.setAlignmentX(JPanel.RIGHT_ALIGNMENT);
179        p.add(modePanel);
180        getToolBar().add(p);
181    }
182
183    /**
184     * For use when the DP3 window is called from another JMRI instance, set
185     * this to prevent the DP3 from shutting down JMRI when the window is
186     * closed.
187     *
188     * @param quitAllowed true if closing window should quit application; false
189     *                    otherwise
190     */
191    protected void allowQuit(boolean quitAllowed) {
192        if (allowQuit != quitAllowed) {
193            newWindowAction = null;
194            allowQuit = quitAllowed;
195            groups.setNewWindowMenuAction(this.getNewWindowAction());
196        }
197
198        firePropertyChange("quit", "setEnabled", allowQuit);
199        //if we are not allowing quit, ie opened from JMRI classic
200        //then we must at least allow the window to be closed
201        if (!allowQuit) {
202            firePropertyChange("closewindow", "setEnabled", true);
203        }
204    }
205
206    JPanel bottomRight() {
207        JPanel panel = new JPanel();
208        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
209        ButtonGroup progMode = new ButtonGroup();
210        progMode.add(service);
211        progMode.add(ops);
212        progMode.add(edit);
213        service.setEnabled(false);
214        ops.setEnabled(false);
215        edit.setEnabled(true);
216        firePropertyChange("setprogservice", "setEnabled", false);
217        firePropertyChange("setprogops", "setEnabled", false);
218        firePropertyChange("setprogedit", "setEnabled", true);
219        ops.setOpaque(false);
220        service.setOpaque(false);
221        edit.setOpaque(false);
222        JPanel progModePanel = new JPanel();
223        GridLayout buttonLayout = new GridLayout(3, 1, 0, 0);
224        progModePanel.setLayout(buttonLayout);
225        progModePanel.add(service);
226        progModePanel.add(ops);
227        progModePanel.add(edit);
228        programModeListener = (ActionEvent e) -> updateProgMode();
229        service.addActionListener(programModeListener);
230        ops.addActionListener(programModeListener);
231        edit.addActionListener(programModeListener);
232        service.setVisible(false);
233        ops.setVisible(false);
234        panel.add(progModePanel);
235        JPanel buttonHolder = new JPanel(new GridBagLayout());
236        GridBagConstraints c = new GridBagConstraints();
237        c.weightx = 1.0;
238        c.fill = GridBagConstraints.HORIZONTAL;
239        c.anchor = GridBagConstraints.NORTH;
240        c.gridx = 0;
241        c.ipady = 20;
242        c.gridwidth = GridBagConstraints.REMAINDER;
243        c.gridy = 0;
244        c.insets = new Insets(2, 2, 2, 2);
245        buttonHolder.add(prog1Button, c);
246        c.weightx = 1;
247        c.fill = GridBagConstraints.NONE;
248        c.gridx = 0;
249        c.gridy = 1;
250        c.gridwidth = 1;
251        c.ipady = 0;
252        buttonHolder.add(rosterMedia, c);
253        c.weightx = 1.0;
254        c.fill = GridBagConstraints.NONE;
255        c.gridx = 1;
256        c.gridy = 1;
257        c.gridwidth = 1;
258        c.ipady = 0;
259        buttonHolder.add(throttleLaunch, c);
260        //buttonHolder.add(throttleLaunch);
261        panel.add(buttonHolder);
262        prog1Button.setEnabled(false);
263        prog1Button.addActionListener((ActionEvent e) -> {
264            log.debug("Open programmer pressed");
265            startProgrammer(null, re, programmer1);
266        });
267
268        rosterMedia.setEnabled(false);
269        rosterMedia.addActionListener((ActionEvent e) -> {
270            log.debug("Open Media pressed");
271            edit.setSelected(true);
272            startProgrammer(null, re, "dp3" + File.separator + "MediaPane");
273        });
274        throttleLaunch.setEnabled(false);
275        throttleLaunch.addActionListener((ActionEvent e) -> {
276            log.debug("Launch Throttle pressed");
277            if (!checkIfEntrySelected()) {
278                return;
279            }
280            ThrottleFrame tf = InstanceManager.getDefault(ThrottleFrameManager.class).createThrottleFrame();
281            tf.toFront();
282            tf.getAddressPanel().setRosterEntry(re);
283        });
284        return panel;
285    }
286
287    protected final void buildWindow() {
288        //Additions to the toolbar need to be added first otherwise when trying to hide bits up during the initialisation they remain on screen
289        additionsToToolBar();
290        frameInstances.add(this);
291        prefsMgr = InstanceManager.getDefault(UserPreferencesManager.class);
292        getTop().add(createTop());
293        getBottom().setMinimumSize(summaryPaneDim);
294        getBottom().add(createBottom());
295        statusBar();
296        systemsMenu();
297        helpMenu(getMenu(), this);
298        if ((!prefsMgr.getSimplePreferenceState(this.getClass().getName() + ".hideGroups")) && !Roster.getDefault().getRosterGroupList().isEmpty()) {
299            hideGroupsPane(false);
300        } else {
301            hideGroupsPane(true);
302        }
303        if (prefsMgr.getSimplePreferenceState(this.getClass().getName() + ".hideSummary")) {
304            //We have to set it to display first, then we can hide it.
305            hideBottomPane(false);
306            hideBottomPane(true);
307        }
308        PropertyChangeListener propertyChangeListener = (PropertyChangeEvent changeEvent) -> {
309            JSplitPane sourceSplitPane = (JSplitPane) changeEvent.getSource();
310            String propertyName = changeEvent.getPropertyName();
311            if (propertyName.equals(JSplitPane.LAST_DIVIDER_LOCATION_PROPERTY)) {
312                int current = sourceSplitPane.getDividerLocation() + sourceSplitPane.getDividerSize();
313                int panesize = (int) (sourceSplitPane.getSize().getHeight());
314                hideBottomPane = panesize - current <= 1;
315                //p.setSimplePreferenceState(DecoderPro3Window.class.getName()+".hideSummary",hideSummary);
316            }
317        };
318        updateProgrammerStatus(null);
319        ConnectionStatus.instance().addPropertyChangeListener((PropertyChangeEvent e) -> {
320            if ((e.getPropertyName().equals("change")) || (e.getPropertyName().equals("add"))) {
321                log.debug("Received property {} with value {} ", e.getPropertyName(), e.getNewValue());
322                updateProgrammerStatus(e);
323            }
324        });
325        InstanceManager.addPropertyChangeListener(InstanceManager.getListPropertyName(AddressedProgrammerManager.class),
326                evt -> {
327                    log.debug("Received property {} with value {} ", evt.getPropertyName(), evt.getNewValue());
328                    AddressedProgrammerManager m = (AddressedProgrammerManager) evt.getNewValue();
329                    if (m != null) {
330                        m.addPropertyChangeListener(this::updateProgrammerStatus);
331                    }
332                    updateProgrammerStatus(evt);
333                });
334        InstanceManager.getList(AddressedProgrammerManager.class).forEach(m -> m.addPropertyChangeListener(this::updateProgrammerStatus));
335        InstanceManager.addPropertyChangeListener(InstanceManager.getListPropertyName(GlobalProgrammerManager.class),
336                evt -> {
337                    log.debug("Received property {} with value {} ", evt.getPropertyName(), evt.getNewValue());
338                    GlobalProgrammerManager m = (GlobalProgrammerManager) evt.getNewValue();
339                    if (m != null) {
340                        m.addPropertyChangeListener(this::updateProgrammerStatus);
341                    }
342                    updateProgrammerStatus(evt);
343                });
344        InstanceManager.getList(GlobalProgrammerManager.class).forEach(m -> m.addPropertyChangeListener(this::updateProgrammerStatus));
345        getSplitPane().addPropertyChangeListener(propertyChangeListener);
346        if (this.getProgrammerConfigManager().getDefaultFile() != null) {
347            programmer1 = this.getProgrammerConfigManager().getDefaultFile();
348        }
349        this.getProgrammerConfigManager().addPropertyChangeListener(ProgrammerConfigManager.DEFAULT_FILE, (PropertyChangeEvent evt) -> {
350            if (this.getProgrammerConfigManager().getDefaultFile() != null) {
351                programmer1 = this.getProgrammerConfigManager().getDefaultFile();
352            }
353        });
354
355        String lastProg = (String) prefsMgr.getProperty(getWindowFrameRef(), "selectedProgrammer");
356        if (lastProg != null) {
357            if (lastProg.equals("service") && service.isEnabled()) {
358                service.setSelected(true);
359                updateProgMode();
360            } else if (lastProg.equals("ops") && ops.isEnabled()) {
361                ops.setSelected(true);
362                updateProgMode();
363            } else if (lastProg.equals("edit") && edit.isEnabled()) {
364                edit.setSelected(true);
365                updateProgMode();
366            }
367        }
368        if (frameInstances.size() > 1) {
369            firePropertyChange("closewindow", "setEnabled", true);
370            allowQuit(frameInstances.get(0).isAllowQuit());
371        } else {
372            firePropertyChange("closewindow", "setEnabled", false);
373        }
374    }
375
376    boolean checkIfEntrySelected() {
377        return this.checkIfEntrySelected(false);
378    }
379
380    boolean checkIfEntrySelected(boolean allowMultiple) {
381        if ((re == null && !allowMultiple) || (this.getSelectedRosterEntries().length < 1)) {
382            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorNoSelection"));
383            return false;
384        }
385        return true;
386    }
387
388    //@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
389    void closeWindow(WindowEvent e) {
390        saveWindowDetails();
391        //Save any changes made in the roster entry details
392        Roster.getDefault().writeRoster();
393        if (allowQuit && frameInstances.size() == 1 && !InstanceManager.getDefault(ShutDownManager.class).isShuttingDown()) {
394            handleQuit(e);
395        } else {
396            //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
397            frameInstances.remove(this);
398            super.windowClosing(e);
399            if ((frameInstances.size() == 1) && (allowQuit)) {
400                frameInstances.get(0).firePropertyChange("closewindow", "setEnabled", false);
401            }
402            dispose();
403        }
404    }
405
406    protected void copyLoco() {
407        CopyRosterItem act = new CopyRosterItem("Copy", this, re);
408        act.actionPerformed(null);
409    }
410
411    JComponent createBottom() {
412        locoImage = new ResizableImagePanel(null, 240, 160);
413        locoImage.setBorder(BorderFactory.createLineBorder(Color.blue));
414        locoImage.setOpaque(true);
415        locoImage.setRespectAspectRatio(true);
416        rosterDetailPanel.setLayout(new BorderLayout());
417        rosterDetailPanel.add(locoImage, BorderLayout.WEST);
418        rosterDetailPanel.add(rosterDetails(), BorderLayout.CENTER);
419        rosterDetailPanel.add(bottomRight(), BorderLayout.EAST);
420        if (prefsMgr.getSimplePreferenceState(this.getClass().getName() + ".hideRosterImage")) {
421            locoImage.setVisible(false);
422            hideRosterImage = true;
423        }
424        rosterEntryUpdateListener = (PropertyChangeEvent e) -> updateDetails();
425        return rosterDetailPanel;
426    }
427
428    private boolean isUpdatingSelection = false;
429
430    JComponent createTop() {
431        Object selectedRosterGroup = prefsMgr.getProperty(getWindowFrameRef(), SELECTED_ROSTER_GROUP);
432        groups = new RosterGroupsPanel((selectedRosterGroup != null) ? selectedRosterGroup.toString() : null);
433        groups.setNewWindowMenuAction(this.getNewWindowAction());
434        setTitle(groups.getSelectedRosterGroup());
435        final JPanel rosters = new JPanel();
436        rosters.setLayout(new BorderLayout());
437        // set up roster table
438        rtable = new RosterTable(true, ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
439        rtable.setRosterGroup(this.getSelectedRosterGroup());
440        rtable.setRosterGroupSource(groups);
441        rosters.add(rtable, BorderLayout.CENTER);
442        // add selection listener
443        rtable.getTable().getSelectionModel().addListSelectionListener((ListSelectionEvent e) -> {
444            JTable table = rtable.getTable();
445            if (!e.getValueIsAdjusting()) {
446                if ((rtable.getSelectedRosterEntries().length == 1 ) && (table.getSelectedRow() >= 0)) {
447                    log.debug("Selected row {}", table.getSelectedRow());
448                    locoSelected(rtable.getModel().getValueAt(table.getRowSorter().convertRowIndexToModel(table.getSelectedRow()), RosterTableModel.IDCOL).toString());
449                } else if (rtable.getSelectedRosterEntries().length > 1) {
450                    log.debug("Multiple selection");
451                    locoSelected(null);
452                } else if ( (table.getSelectedRow() < 0) && (!isUpdatingSelection) ) {
453                    isUpdatingSelection = true;
454                    if (re != null) { // can be null with multiple selection
455                        log.debug("Selected roster entry {}", re.getId());
456                        if (!rtable.setSelection(re)) {
457                            re = null; //nothng was found
458                        }
459                    }
460                    updateDetails();
461                    rtable.moveTableViewToSelected();
462                    isUpdatingSelection = false;
463                } // leave last selected item visible if no selection
464            }
465        });
466
467        //Set all the sort and width details of the table first.
468        String rostertableref = getWindowFrameRef() + ":roster";
469        rtable.getTable().setName(rostertableref);
470
471        // Allow only one column to be sorted at a time -
472        // Java allows multiple column sorting, but to effectively persist that, we
473        // need to be intelligent about which columns can be meaningfully sorted
474        // with other columns; this bypasses the problem by only allowing the
475        // last column sorted to affect sorting
476        RowSorterUtil.addSingleSortableColumnListener(rtable.getTable().getRowSorter());
477
478        // Reset and then persist the table's ui state
479        JTablePersistenceManager tpm = InstanceManager.getNullableDefault(JTablePersistenceManager.class);
480        if (tpm != null) {
481            tpm.resetState(rtable.getTable());
482            tpm.persist(rtable.getTable());
483        }
484        rtable.getTable().setDragEnabled(true);
485        rtable.getTable().setTransferHandler(new TransferHandler() {
486
487            @Override
488            public int getSourceActions(JComponent c) {
489                return TransferHandler.COPY;
490            }
491
492            @Override
493            public Transferable createTransferable(JComponent c) {
494                JTable table = rtable.getTable();
495                ArrayList<String> Ids = new ArrayList<>(table.getSelectedRowCount());
496                for (int i = 0; i < table.getSelectedRowCount(); i++) {
497                    Ids.add(rtable.getModel().getValueAt(table.getRowSorter().convertRowIndexToModel(table.getSelectedRows()[i]), RosterTableModel.IDCOL).toString());
498                }
499                return new RosterEntrySelection(Ids);
500            }
501
502            @Override
503            public void exportDone(JComponent c, Transferable t, int action) {
504                // nothing to do
505            }
506        });
507        JmriMouseListener rosterMouseListener = new RosterPopupListener();
508        rtable.getTable().addMouseListener(JmriMouseListener.adapt(rosterMouseListener));
509
510        // assemble roster/groups splitpane
511        rosterGroupSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, groups, rosters);
512        rosterGroupSplitPane.setOneTouchExpandable(true);
513        rosterGroupSplitPane.setResizeWeight(0); // emphasis rosters
514        Object w = prefsMgr.getProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation");
515        if (w != null) {
516            groupSplitPaneLocation = (Integer) w;
517            rosterGroupSplitPane.setDividerLocation(groupSplitPaneLocation);
518        }
519        if (!Roster.getDefault().getRosterGroupList().isEmpty()) {
520            if (prefsMgr.getSimplePreferenceState(this.getClass().getName() + ".hideGroups")) {
521                hideGroupsPane(true);
522            }
523        } else {
524            enableRosterGroupMenuItems(false);
525        }
526        PropertyChangeListener propertyChangeListener = (PropertyChangeEvent changeEvent) -> {
527            JSplitPane sourceSplitPane = (JSplitPane) changeEvent.getSource();
528            String propertyName = changeEvent.getPropertyName();
529            if (propertyName.equals(JSplitPane.LAST_DIVIDER_LOCATION_PROPERTY)) {
530                int current = sourceSplitPane.getDividerLocation();
531                hideGroups = current <= 1;
532                Integer last = (Integer) changeEvent.getNewValue();
533                if (current >= 2) {
534                    groupSplitPaneLocation = current;
535                } else if (last >= 2) {
536                    groupSplitPaneLocation = last;
537                }
538            }
539        };
540        groups.addPropertyChangeListener(SELECTED_ROSTER_GROUP, new PropertyChangeListener() {
541            @Override
542            public void propertyChange(PropertyChangeEvent pce) {
543                prefsMgr.setProperty(this.getClass().getName(), SELECTED_ROSTER_GROUP, pce.getNewValue());
544                setTitle((String) pce.getNewValue());
545            }
546        });
547        rosterGroupSplitPane.addPropertyChangeListener(propertyChangeListener);
548        Roster.getDefault().addPropertyChangeListener((PropertyChangeEvent e) -> {
549            if (e.getPropertyName().equals("RosterGroupAdded") && Roster.getDefault().getRosterGroupList().size() == 1) {
550                // if the pane is hidden, show it when 1st group is created
551                hideGroupsPane(false);
552                enableRosterGroupMenuItems(true);
553            } else if (!rtable.isVisible() && (e.getPropertyName().equals("saved"))) {
554                if (firstHelpLabel != null) {
555                    firstHelpLabel.setVisible(false);
556                }
557                rtable.setVisible(true);
558                rtable.resetColumnWidths();
559            }
560        });
561        if (Roster.getDefault().numEntries() == 0) {
562            try {
563                BufferedImage myPicture = ImageIO.read(FileUtil.findURL(("resources/" + Bundle.getMessage("ThrottleFirstUseImage")), FileUtil.Location.INSTALLED));
564                //rosters.add(new JLabel(new ImageIcon( myPicture )), BorderLayout.CENTER);
565                firstHelpLabel = new JLabel(new ImageIcon(myPicture));
566                rtable.setVisible(false);
567                rosters.add(firstHelpLabel, BorderLayout.NORTH);
568                //tableArea.add(firstHelpLabel);
569                rtable.setVisible(false);
570            } catch (IOException ex) {
571                // handle exception...
572            }
573        }
574        return rosterGroupSplitPane;
575    }
576
577    protected void deleteLoco() {
578        DeleteRosterItemAction act = new DeleteRosterItemAction("Delete", (WindowInterface) this);
579        act.actionPerformed(null);
580    }
581
582    void editMediaButton() {
583        //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.
584        boolean serviceSelected = service.isSelected();
585        boolean opsSelected = ops.isSelected();
586        edit.setSelected(true);
587        startProgrammer(null, re, "dp3" + File.separator + "MediaPane");
588        service.setSelected(serviceSelected);
589        ops.setSelected(opsSelected);
590    }
591
592    protected void enableRosterGroupMenuItems(boolean enable) {
593        firePropertyChange("groupspane", "setEnabled", enable);
594        firePropertyChange("grouptable", "setEnabled", enable);
595        firePropertyChange("deletegroup", "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            ThrottleFrame tf = InstanceManager.getDefault(ThrottleFrameManager.class).createThrottleFrame();
1297            tf.toFront();
1298            tf.getAddressPanel().getRosterEntrySelector().setSelectedRosterGroup(getSelectedRosterGroup());
1299            tf.getAddressPanel().setRosterEntry(re);
1300        });
1301        if (re == null) {
1302            menuItem.setEnabled(false);
1303        }
1304        popupMenu.add(menuItem);
1305        popupMenu.addSeparator();
1306
1307        menuItem = new JMenuItem(Bundle.getMessage("PrintSelection"));
1308        menuItem.addActionListener((ActionEvent e1) -> printLoco(false));
1309        if (re == null) {
1310            menuItem.setEnabled(false);
1311        }
1312        popupMenu.add(menuItem);
1313        menuItem = new JMenuItem(Bundle.getMessage("PreviewSelection"));
1314        menuItem.addActionListener((ActionEvent e1) -> printLoco(true));
1315        if (re == null) {
1316            menuItem.setEnabled(false);
1317        }
1318        popupMenu.add(menuItem);
1319        popupMenu.addSeparator();
1320
1321        menuItem = new JMenuItem(Bundle.getMessage("Duplicateddd"));
1322        menuItem.addActionListener((ActionEvent e1) -> copyLoco());
1323        if (re == null) {
1324            menuItem.setEnabled(false);
1325        }
1326        popupMenu.add(menuItem);
1327        boolean deleteFromGroup = this.getSelectedRosterGroup() != null && !this.getSelectedRosterGroup().equals(Roster.NOGROUP);
1328        menuItem = new JMenuItem(deleteFromGroup ? Bundle.getMessage("DeleteFromGroup") : Bundle.getMessage("DeleteFromRoster")); // NOI18N
1329        menuItem.addActionListener((ActionEvent e1) -> deleteLoco());
1330        popupMenu.add(menuItem);
1331        menuItem.setEnabled(this.getSelectedRosterEntries().length > 0);
1332
1333        popupMenu.show(e.getComponent(), e.getX(), e.getY());
1334    }
1335
1336    /**
1337     * Start the identify operation after [Identify Loco] button pressed.
1338     * <p>
1339     * This defines what happens when Identify is done.
1340     */
1341    //taken out of CombinedLocoSelPane
1342    protected void startIdentifyLoco() {
1343        final RosterFrame me = this;
1344        Programmer programmer = null;
1345        if (modePanel.isSelected()) {
1346            programmer = modePanel.getProgrammer();
1347        }
1348        if (programmer == null) {
1349            GlobalProgrammerManager gpm = InstanceManager.getNullableDefault(GlobalProgrammerManager.class);
1350            if (gpm != null) {
1351                programmer = gpm.getGlobalProgrammer();
1352                log.warn("Selector did not provide a programmer, attempt to use GlobalProgrammerManager default: {}", programmer);
1353            } else {
1354                log.warn("Selector did not provide a programmer, and no ProgramManager found in InstanceManager");
1355            }
1356        }
1357
1358        // if failed to get programmer, tell user and stop
1359        if (programmer == null) {
1360            log.error("Identify loco called when no service mode programmer is available; button should have been disabled");
1361            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("IdentifyError"));
1362            return;
1363        }
1364
1365        // and now do the work
1366        IdentifyLoco ident = new IdentifyLoco(programmer) {
1367            private final RosterFrame who = me;
1368
1369            @Override
1370            protected void done(int dccAddress) {
1371                // if Done, updated the selected decoder
1372                // on the GUI thread, right now
1373                jmri.util.ThreadingUtil.runOnGUI(() -> who.selectLoco(dccAddress, !shortAddr, cv8val, cv7val));
1374            }
1375
1376            @Override
1377            protected void message(String m) {
1378                // on the GUI thread, right now
1379                jmri.util.ThreadingUtil.runOnGUI(() -> statusField.setText(m));
1380            }
1381
1382            @Override
1383            protected void error() {
1384                // raise the button again
1385                //idloco.setSelected(false);
1386            }
1387        };
1388        ident.start();
1389    }
1390
1391    protected void startProgrammer(DecoderFile decoderFile, RosterEntry re, String filename) {
1392        if (inStartProgrammer) {
1393            log.debug("Call to start programmer has been called twice when the first call hasn't opened");
1394            return;
1395        }
1396        if (!checkIfEntrySelected()) {
1397            return;
1398        }
1399        try {
1400            setCursor(new Cursor(Cursor.WAIT_CURSOR));
1401            inStartProgrammer = true;
1402            String title = re.getId();
1403            JFrame progFrame = null;
1404            if (edit.isSelected()) {
1405                progFrame = new PaneProgFrame(decoderFile, re, title, "programmers" + File.separator + filename + ".xml", null, false) {
1406                    @Override
1407                    protected JPanel getModePane() {
1408                        return null;
1409                    } // hide prog mode buttons pane
1410                };
1411            } else if (service.isSelected()) {
1412                progFrame = new PaneServiceProgFrame(decoderFile, re, title, "programmers" + File.separator + filename + ".xml", modePanel.getProgrammer());
1413            } else if (ops.isSelected()) {
1414                int address = Integer.parseInt(re.getDccAddress());
1415                boolean longAddr = re.isLongAddress();
1416                Programmer pProg = InstanceManager.getDefault(AddressedProgrammerManager.class).getAddressedProgrammer(longAddr, address);
1417                progFrame = new PaneOpsProgFrame(decoderFile, re, title, "programmers" + File.separator + filename + ".xml", pProg);
1418            }
1419            if (progFrame == null) {
1420                return;
1421            }
1422            progFrame.pack();
1423            progFrame.setVisible(true);
1424        } finally {
1425            setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
1426        }
1427        inStartProgrammer = false;
1428    }
1429
1430    /**
1431     * Create and display a status bar along the bottom edge of the Roster main
1432     * pane.
1433     * <p>
1434     * TODO This status bar needs sorting out properly
1435     */
1436    protected void statusBar() {
1437        addToStatusBox(serviceModeProgrammerLabel, null);
1438        addToStatusBox(operationsModeProgrammerLabel, null);
1439        JLabel programmerStatusLabel = new JLabel(Bundle.getMessage("ProgrammerStatus"));
1440        statusField.setText(Bundle.getMessage("StateIdle"));
1441        addToStatusBox(programmerStatusLabel, statusField);
1442        Profile profile = ProfileManager.getDefault().getActiveProfile();
1443        if (profile != null) {
1444            addToStatusBox(new JLabel(Bundle.getMessage("ActiveProfile", profile.getName())), null);
1445        }
1446    }
1447
1448    protected void systemsMenu() {
1449        ActiveSystemsMenu.addItems(getMenu());
1450        getMenu().add(new WindowMenu(this));
1451    }
1452
1453    void updateDetails() {
1454        if (re == null) {
1455            String value = (rtable.getTable().getSelectedRowCount() > 1) ? "Multiple Items Selected" : "";
1456            filename.setText(value);
1457            dateUpdated.setText(value);
1458            decoderModel.setText(value);
1459            decoderFamily.setText(value);
1460            id.setText(value);
1461            roadName.setText(value);
1462            dccAddress.setText(value);
1463            roadNumber.setText(value);
1464            mfg.setText(value);
1465            model.setText(value);
1466            owner.setText(value);
1467            locoImage.setImagePath(null);
1468            service.setEnabled(false);
1469            ops.setEnabled(false);
1470            edit.setEnabled(false);
1471            prog1Button.setEnabled(false);
1472            prog2Button.setEnabled(false);
1473            throttleLabels.setEnabled(false);
1474            rosterMedia.setEnabled(false);
1475            throttleLaunch.setEnabled(false );
1476        } else {
1477            filename.setText(re.getFileName());
1478            dateUpdated.setText((re.getDateModified() != null)
1479                    ? DateFormat.getDateTimeInstance().format(re.getDateModified())
1480                    : re.getDateUpdated());
1481            decoderModel.setText(re.getDecoderModel());
1482            decoderFamily.setText(re.getDecoderFamily());
1483            dccAddress.setText(re.getDccAddress());
1484            id.setText(re.getId());
1485            roadName.setText(re.getRoadName());
1486            roadNumber.setText(re.getRoadNumber());
1487            mfg.setText(re.getMfg());
1488            model.setText(re.getModel());
1489            owner.setText(re.getOwner());
1490            locoImage.setImagePath(re.getImagePath());
1491            if (hideRosterImage) {
1492                locoImage.setVisible(false);
1493            } else {
1494                locoImage.setVisible(true);
1495            }
1496            service.setEnabled(isProgrammingTrackEnabled());
1497            ops.setEnabled(isProgrammingOnMainEnabled());
1498            edit.setEnabled(true);
1499            prog1Button.setEnabled(true);
1500            prog2Button.setEnabled(true);
1501            throttleLabels.setEnabled(true);
1502            rosterMedia.setEnabled(true);
1503            throttleLaunch.setEnabled(true);
1504            updateProgMode();
1505        }
1506    }
1507
1508    void updateProgMode() {
1509        String progMode;
1510        if (service.isSelected()) {
1511            progMode = "setprogservice";
1512        } else if (ops.isSelected()) {
1513            progMode = "setprogops";
1514        } else {
1515            progMode = "setprogedit";
1516        }
1517        firePropertyChange(progMode, "setSelected", true);
1518    }
1519
1520    /**
1521     * Handle setting up and updating the GUI for the types of programmer
1522     * available.
1523     *
1524     * @param evt the triggering event; if not null and if a removal of a
1525     *            ProgrammerManager, care will be taken not to trigger the
1526     *            automatic creation of a new ProgrammerManager
1527     */
1528    protected void updateProgrammerStatus(@CheckForNull PropertyChangeEvent evt) {
1529        log.debug("Updating Programmer Status");
1530        ConnectionConfig oldServMode = serModeProCon;
1531        ConnectionConfig oldOpsMode = opsModeProCon;
1532        GlobalProgrammerManager gpm = null;
1533        AddressedProgrammerManager apm = null;
1534
1535        // Find the connection that goes with the global programmer
1536        // test that IM has a default GPM, or that event is not the removal of a GPM
1537        if (InstanceManager.containsDefault(GlobalProgrammerManager.class)
1538                || (evt != null
1539                && evt.getPropertyName().equals(InstanceManager.getDefaultsPropertyName(GlobalProgrammerManager.class))
1540                && evt.getNewValue() == null)) {
1541            gpm = InstanceManager.getNullableDefault(GlobalProgrammerManager.class);
1542            log.trace("found global programming manager {}", gpm);
1543        }
1544        if (gpm != null) {
1545            String serviceModeProgrammerName = gpm.getUserName();
1546            log.debug("GlobalProgrammerManager found of class {} name {} ", gpm.getClass(), serviceModeProgrammerName);
1547            InstanceManager.getOptionalDefault(ConnectionConfigManager.class).ifPresent((ccm) -> {
1548                for (ConnectionConfig connection : ccm) {
1549                    log.debug("Checking connection name {}", connection.getConnectionName());
1550                    if (connection.getConnectionName() != null && connection.getConnectionName().equals(serviceModeProgrammerName)) {
1551                        log.debug("Connection found for GlobalProgrammermanager");
1552                        serModeProCon = connection;
1553                    }
1554                }
1555            });
1556        }
1557
1558        // Find the connection that goes with the addressed programmer
1559        // test that IM has a default APM, or that event is not the removal of an APM
1560        if (InstanceManager.containsDefault(AddressedProgrammerManager.class)
1561                || (evt != null
1562                && evt.getPropertyName().equals(InstanceManager.getDefaultsPropertyName(AddressedProgrammerManager.class))
1563                && evt.getNewValue() == null)) {
1564            apm = InstanceManager.getNullableDefault(AddressedProgrammerManager.class);
1565            log.trace("found addressed programming manager {}", gpm);
1566        }
1567        if (apm != null) {
1568            String opsModeProgrammerName = apm.getUserName();
1569            log.debug("AddressedProgrammerManager found of class {} name {} ", apm.getClass(), opsModeProgrammerName);
1570            InstanceManager.getOptionalDefault(ConnectionConfigManager.class).ifPresent((ccm) -> {
1571                for (ConnectionConfig connection : ccm) {
1572                    log.debug("Checking connection name {}", connection.getConnectionName());
1573                    if (connection.getConnectionName() != null && connection.getConnectionName().equals(opsModeProgrammerName)) {
1574                        log.debug("Connection found for AddressedProgrammermanager");
1575                        opsModeProCon = connection;
1576                    }
1577                }
1578            });
1579        }
1580
1581        log.trace("start global check with {}, {}, {}", serModeProCon, gpm, (gpm != null ? gpm.isGlobalProgrammerAvailable() : "<none>"));
1582        if (serModeProCon != null && gpm != null && gpm.isGlobalProgrammerAvailable()) {
1583            if (ConnectionStatus.instance().isConnectionOk(serModeProCon.getConnectionName(), serModeProCon.getInfo())) {
1584                log.debug("GPM Connection online 1");
1585                serviceModeProgrammerLabel.setText(
1586                        Bundle.getMessage("ServiceModeProgOnline", serModeProCon.getConnectionName()));
1587                serviceModeProgrammerLabel.setForeground(new Color(0, 128, 0));
1588            } else {
1589                log.debug("GPM Connection offline");
1590                serviceModeProgrammerLabel.setText(
1591                        Bundle.getMessage("ServiceModeProgOffline", serModeProCon.getConnectionName()));
1592                serviceModeProgrammerLabel.setForeground(Color.red);
1593            }
1594            if (oldServMode == null) {
1595                log.debug("Re-enable user interface");
1596                contextService.setEnabled(isProgrammingTrackEnabled());
1597                contextService.setVisible(true);
1598                service.setEnabled(isProgrammingTrackEnabled());
1599                service.setVisible(true);
1600                firePropertyChange("setprogservice", "setEnabled", true);
1601                getToolBar().getComponents()[1].setEnabled(true);
1602            }
1603        } else if (gpm != null && gpm.isGlobalProgrammerAvailable()) {
1604            if (ConnectionStatus.instance().isSystemOk(gpm.getUserName())) {
1605                log.debug("GPM Connection online 2");
1606                serviceModeProgrammerLabel.setText(
1607                        Bundle.getMessage("ServiceModeProgOnline", gpm.getUserName()));
1608                serviceModeProgrammerLabel.setForeground(new Color(0, 128, 0));
1609            } else {
1610                log.debug("GPM Connection onffline");
1611                serviceModeProgrammerLabel.setText(
1612                        Bundle.getMessage("ServiceModeProgOffline", gpm.getUserName()));
1613                serviceModeProgrammerLabel.setForeground(Color.red);
1614            }
1615            if (oldServMode == null) {
1616                log.debug("Re-enable user interface");
1617                contextService.setEnabled(isProgrammingTrackEnabled());
1618                contextService.setVisible(true);
1619                service.setEnabled(isProgrammingTrackEnabled());
1620                service.setVisible(true);
1621                firePropertyChange("setprogservice", "setEnabled", true);
1622                getToolBar().getComponents()[1].setEnabled(true);
1623            }
1624        } else {
1625            // No service programmer available, disable interface sections not available
1626            log.debug("no service programmer");
1627            serviceModeProgrammerLabel.setText(Bundle.getMessage("NoServiceProgrammerAvailable"));
1628            serviceModeProgrammerLabel.setForeground(Color.red);
1629            if (oldServMode != null) {
1630                contextService.setEnabled(false);
1631                contextService.setVisible(false);
1632                service.setEnabled(false);
1633                service.setVisible(false);
1634                firePropertyChange("setprogservice", "setEnabled", false);
1635            }
1636            // Disable Identify in toolBar
1637            // This relies on it being the 2nd item in the toolbar, as defined in xml//config/parts/jmri/jmrit/roster/swing/RosterFrameToolBar.xml
1638            // Because of I18N, we don't look for a particular Action name here
1639            getToolBar().getComponents()[1].setEnabled(false);
1640            serModeProCon = null;
1641        }
1642
1643        if (opsModeProCon != null && apm != null && apm.isAddressedModePossible()) {
1644            if (ConnectionStatus.instance().isConnectionOk(opsModeProCon.getConnectionName(), opsModeProCon.getInfo())) {
1645                log.debug("Ops Mode Connection online");
1646                operationsModeProgrammerLabel.setText(
1647                        Bundle.getMessage("OpsModeProgOnline", opsModeProCon.getConnectionName()));
1648                operationsModeProgrammerLabel.setForeground(new Color(0, 128, 0));
1649            } else {
1650                log.debug("Ops Mode Connection offline");
1651                operationsModeProgrammerLabel.setText(
1652                        Bundle.getMessage("OpsModeProgOffline", opsModeProCon.getConnectionName()));
1653                operationsModeProgrammerLabel.setForeground(Color.red);
1654            }
1655            if (oldOpsMode == null) {
1656                contextOps.setEnabled(isProgrammingOnMainEnabled());
1657                contextOps.setVisible(true);
1658                ops.setEnabled(isProgrammingOnMainEnabled());
1659                ops.setVisible(true);
1660                firePropertyChange("setprogops", "setEnabled", true);
1661            }
1662        } else if (apm != null && apm.isAddressedModePossible()) {
1663            if (ConnectionStatus.instance().isSystemOk(apm.getUserName())) {
1664                log.debug("Ops Mode Connection online");
1665                operationsModeProgrammerLabel.setText(
1666                        Bundle.getMessage("OpsModeProgOnline", apm.getUserName()));
1667                operationsModeProgrammerLabel.setForeground(new Color(0, 128, 0));
1668            } else {
1669                log.debug("Ops Mode Connection offline");
1670                operationsModeProgrammerLabel.setText(
1671                        Bundle.getMessage("OpsModeProgOffline", apm.getUserName()));
1672                operationsModeProgrammerLabel.setForeground(Color.red);
1673            }
1674            if (oldOpsMode == null) {
1675                contextOps.setEnabled(isProgrammingOnMainEnabled());
1676                contextOps.setVisible(true);
1677                ops.setEnabled(isProgrammingOnMainEnabled());
1678                ops.setVisible(true);
1679                firePropertyChange("setprogops", "setEnabled", true);
1680            }
1681        } else {
1682            // No ops mode programmer available, disable interface sections not available
1683            log.debug("no ops mode programmer");
1684            operationsModeProgrammerLabel.setText(Bundle.getMessage("NoOpsProgrammerAvailable"));
1685            operationsModeProgrammerLabel.setForeground(Color.red);
1686            if (oldOpsMode != null) {
1687                contextOps.setEnabled(false);
1688                contextOps.setVisible(false);
1689                ops.setEnabled(false);
1690                ops.setVisible(false);
1691                firePropertyChange("setprogops", "setEnabled", false);
1692            }
1693            opsModeProCon = null;
1694        }
1695        String strProgMode;
1696        if (service.isEnabled()) {
1697            contextService.setSelected(true);
1698            service.setSelected(true);
1699            strProgMode = "setprogservice";
1700            modePanel.setVisible(true);
1701        } else if (ops.isEnabled()) {
1702            contextOps.setSelected(true);
1703            ops.setSelected(true);
1704            strProgMode = "setprogops";
1705            modePanel.setVisible(false);
1706        } else {
1707            contextEdit.setSelected(true);
1708            edit.setSelected(true);
1709            modePanel.setVisible(false);
1710            strProgMode = "setprogedit";
1711        }
1712        firePropertyChange(strProgMode, "setSelected", true);
1713    }
1714
1715    private boolean isProgrammingTrackEnabled() {
1716        return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null &&
1717                ! InstanceManager.getDefault(ProgrammerConfigManager.class).isDisableProgrammingTrack();
1718    }
1719
1720    private boolean isProgrammingOnMainEnabled() {
1721        return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null &&
1722                ! InstanceManager.getDefault(ProgrammerConfigManager.class).isDisableProgrammingOnMain();
1723    }
1724
1725    @Override
1726    public void windowClosing(WindowEvent e) {
1727        closeWindow(e);
1728        super.windowClosing(e);
1729    }
1730
1731    /**
1732     * Displays a context (right-click) menu for a roster entry.
1733     */
1734    private class RosterPopupListener extends JmriMouseAdapter {
1735
1736        @Override
1737        public void mousePressed(JmriMouseEvent e) {
1738            if (e.isPopupTrigger()) {
1739                showPopup(e);
1740            }
1741        }
1742
1743        @Override
1744        public void mouseReleased(JmriMouseEvent e) {
1745            if (e.isPopupTrigger()) {
1746                showPopup(e);
1747            }
1748        }
1749
1750        @Override
1751        public void mouseClicked(JmriMouseEvent e) {
1752            if (e.isPopupTrigger()) {
1753                showPopup(e);
1754                return;
1755            }
1756            if (e.getClickCount() == 2) {
1757                startProgrammer(null, re, programmer1);
1758            }
1759        }
1760    }
1761
1762    private static class ExportRosterItem extends ExportRosterItemAction {
1763
1764        ExportRosterItem(String pName, Component pWho, RosterEntry re) {
1765            super(pName, pWho);
1766            super.setExistingEntry(re);
1767        }
1768
1769        @Override
1770        protected boolean selectFrom() {
1771            return true;
1772        }
1773    }
1774
1775    private static class CopyRosterItem extends CopyRosterItemAction {
1776
1777        CopyRosterItem(String pName, Component pWho, RosterEntry re) {
1778            super(pName, pWho);
1779            super.setExistingEntry(re);
1780        }
1781
1782        @Override
1783        protected boolean selectFrom() {
1784            return true;
1785        }
1786    }
1787    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterFrame.class);
1788
1789}