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("addgroup", "setEnabled", enable);
599    }
600
601    protected void exportLoco() {
602        ExportRosterItem act = new ExportRosterItem(Bundle.getMessage("Export"), this, re);
603        act.actionPerformed(null);
604    }
605
606    void formatTextAreaAsLabel(JTextPane pane) {
607        pane.setOpaque(false);
608        pane.setEditable(false);
609        pane.setBorder(null);
610    }
611
612    /*=============== Getters and Setters for core properties ===============*/
613
614    /**
615     * @return Will closing the window quit JMRI?
616     */
617    public boolean isAllowQuit() {
618        return allowQuit;
619    }
620
621    /**
622     * @param allowQuit Set state to either close JMRI or just the roster window
623     */
624    public void setAllowQuit(boolean allowQuit) {
625        allowQuit(allowQuit);
626    }
627
628    /**
629     * @return the baseTitle
630     */
631    protected String getBaseTitle() {
632        return baseTitle;
633    }
634
635    /**
636     * @param baseTitle the baseTitle to set
637     */
638    protected final void setBaseTitle(String baseTitle) {
639        String title = null;
640        if (this.baseTitle == null) {
641            title = this.getTitle();
642        }
643        this.baseTitle = baseTitle;
644        if (title != null) {
645            this.setTitle(title);
646        }
647    }
648
649    /**
650     * @return the newWindowAction
651     */
652    protected JmriAbstractAction getNewWindowAction() {
653        if (newWindowAction == null) {
654            newWindowAction = new RosterFrameAction("newWindow", this, allowQuit);
655        }
656        return newWindowAction;
657    }
658
659    /**
660     * @param newWindowAction the newWindowAction to set
661     */
662    protected void setNewWindowAction(JmriAbstractAction newWindowAction) {
663        this.newWindowAction = newWindowAction;
664        this.groups.setNewWindowMenuAction(newWindowAction);
665    }
666
667    @Override
668    public void setTitle(String title) {
669        if (title == null || title.isEmpty()) {
670            title = Roster.ALLENTRIES;
671        }
672        if (this.baseTitle != null) {
673            if (!title.equals(this.baseTitle) && !title.startsWith(this.baseTitle)) {
674                super.setTitle(this.baseTitle + ": " + title);
675            }
676        } else {
677            super.setTitle(title);
678        }
679    }
680
681    @Override
682    public Object getProperty(String key) {
683        if (key.equalsIgnoreCase(SELECTED_ROSTER_GROUP)) {
684            return getSelectedRosterGroup();
685        } else if (key.equalsIgnoreCase("hideSummary")) {
686            return hideBottomPane;
687        }
688        // call parent getProperty method to return any properties defined
689        // in the class hierarchy.
690        return super.getProperty(key);
691    }
692
693    public Object getRemoteObject(String value) {
694        return getProperty(value);
695    }
696
697    @Override
698    public RosterEntry[] getSelectedRosterEntries() {
699        RosterEntry[] entries = rtable.getSelectedRosterEntries();
700        return Arrays.copyOf(entries, entries.length);
701    }
702
703    public RosterEntry[] getAllRosterEntries() {
704        RosterEntry[] entries = rtable.getSortedRosterEntries();
705        return Arrays.copyOf(entries, entries.length);
706    }
707
708    @Override
709    public String getSelectedRosterGroup() {
710        return groups.getSelectedRosterGroup();
711    }
712
713    protected ProgrammerConfigManager getProgrammerConfigManager() {
714        return InstanceManager.getDefault(ProgrammerConfigManager.class);
715    }
716
717    void handleQuit(WindowEvent e) {
718        if (e != null && frameInstances.size() == 1) {
719            final String rememberWindowClose = this.getClass().getName() + ".closeDP3prompt";
720            if (!prefsMgr.getSimplePreferenceState(rememberWindowClose)) {
721                JPanel message = new JPanel();
722                JLabel question = new JLabel(rb.getString("MessageLongCloseWarning"));
723                final JCheckBox remember = new JCheckBox(rb.getString("MessageRememberSetting"));
724                remember.setFont(remember.getFont().deriveFont(10.0F));
725                message.setLayout(new BoxLayout(message, BoxLayout.Y_AXIS));
726                message.add(question);
727                message.add(remember);
728                int result = JmriJOptionPane.showConfirmDialog(null,
729                        message,
730                        rb.getString("MessageShortCloseWarning"),
731                        JmriJOptionPane.YES_NO_OPTION);
732                if (remember.isSelected()) {
733                    prefsMgr.setSimplePreferenceState(rememberWindowClose, true);
734                }
735                if (result == JmriJOptionPane.YES_OPTION) {
736                    handleQuit();
737                }
738            } else {
739                handleQuit();
740            }
741        } else if (frameInstances.size() > 1) {
742            final String rememberWindowClose = this.getClass().getName() + ".closeMultipleDP3prompt";
743            if (!prefsMgr.getSimplePreferenceState(rememberWindowClose)) {
744                JPanel message = new JPanel();
745                JLabel question = new JLabel(rb.getString("MessageLongMultipleCloseWarning"));
746                final JCheckBox remember = new JCheckBox(rb.getString("MessageRememberSetting"));
747                remember.setFont(remember.getFont().deriveFont(10.0F));
748                message.setLayout(new BoxLayout(message, BoxLayout.Y_AXIS));
749                message.add(question);
750                message.add(remember);
751                int result = JmriJOptionPane.showConfirmDialog(null,
752                        message,
753                        rb.getString("MessageShortCloseWarning"),
754                        JmriJOptionPane.YES_NO_OPTION);
755                if (remember.isSelected()) {
756                    prefsMgr.setSimplePreferenceState(rememberWindowClose, true);
757                }
758                if (result == JmriJOptionPane.YES_OPTION) {
759                    handleQuit();
760                }
761            } else {
762                handleQuit();
763            }
764            //closeWindow(null);
765        }
766    }
767
768    private void handleQuit(){
769        try {
770            InstanceManager.getDefault(jmri.ShutDownManager.class).shutdown();
771        } catch (Exception e) {
772            log.error("Continuing after error in handleQuit", e);
773        }
774    }
775
776    protected void helpMenu(JMenuBar menuBar, final JFrame frame) {
777        // create menu and standard items
778        JMenu helpMenu = HelpUtil.makeHelpMenu("package.apps.gui3.dp3.DecoderPro3", true);
779        // use as main help menu
780        menuBar.add(helpMenu);
781    }
782
783    protected void hideGroups() {
784        boolean boo = !hideGroups;
785        hideGroupsPane(boo);
786    }
787
788    public void hideGroupsPane(boolean hide) {
789        if (hideGroups == hide) {
790            return;
791        }
792        hideGroups = hide;
793        if (hide) {
794            groupSplitPaneLocation = rosterGroupSplitPane.getDividerLocation();
795            rosterGroupSplitPane.setDividerLocation(1);
796            rosterGroupSplitPane.getLeftComponent().setMinimumSize(new Dimension());
797            if (Roster.getDefault().getRosterGroupList().isEmpty()) {
798                rosterGroupSplitPane.setOneTouchExpandable(false);
799                rosterGroupSplitPane.setDividerSize(0);
800            }
801        } else {
802            rosterGroupSplitPane.setDividerSize(UIManager.getInt("SplitPane.dividerSize"));
803            rosterGroupSplitPane.setOneTouchExpandable(true);
804            if (groupSplitPaneLocation >= 2) {
805                rosterGroupSplitPane.setDividerLocation(groupSplitPaneLocation);
806            } else {
807                rosterGroupSplitPane.resetToPreferredSizes();
808            }
809        }
810    }
811
812    protected void hideRosterImage() {
813        hideRosterImage = !hideRosterImage;
814        //p.setSimplePreferenceState(DecoderPro3Window.class.getName()+".hideRosterImage",hideRosterImage);
815        if (hideRosterImage) {
816            locoImage.setVisible(false);
817        } else {
818            locoImage.setVisible(true);
819        }
820    }
821
822    protected void hideSummary() {
823        boolean boo = !hideBottomPane;
824        hideBottomPane(boo);
825    }
826
827    /**
828     * An entry has been selected in the Roster Table, activate the bottom part
829     * of the window.
830     *
831     * @param id ID of the selected roster entry
832     */
833    final void locoSelected(String id) {
834        if (id != null) {
835            log.debug("locoSelected ID {}", id);
836            if (re != null) {
837                // we remove the propertychangelistener if we had a previously selected entry;
838                re.removePropertyChangeListener(rosterEntryUpdateListener);
839            }
840            // convert to roster entry
841            re = Roster.getDefault().entryFromTitle(id);
842            re.addPropertyChangeListener(rosterEntryUpdateListener);
843        } else {
844            log.debug("Multiple selection");
845            re = null;
846        }
847        updateDetails();
848    }
849
850    protected void newWindow() {
851        this.newWindow(this.getNewWindowAction());
852    }
853
854    protected void newWindow(JmriAbstractAction action) {
855        action.setWindowInterface(this);
856        action.actionPerformed(null);
857        firePropertyChange("closewindow", "setEnabled", true);
858    }
859
860    /**
861     * Prepare a roster entry to be printed, and display a selection list.
862     *
863     * @see jmri.jmrit.roster.PrintRosterEntry#printPanes(boolean)
864     * @param preview true if output should go to a Preview pane on screen, false
865     *            to output to a printer (dialog)
866     */
867    protected void printLoco(boolean preview) {
868        log.debug("Selected entry: {}", re.getDisplayName());
869        String programmer = "Basic";
870        if (this.getProgrammerConfigManager().getDefaultFile() != null) {
871            programmer = this.getProgrammerConfigManager().getDefaultFile();
872        } else {
873            log.error("programmer is NULL");
874        }
875        PrintRosterEntry pre = new PrintRosterEntry(re, this, "programmers" + File.separator + programmer + ".xml");
876        // uses programmer set in prefs when printing a selected entry from (this) top Roster frame
877        // compare with: jmri.jmrit.symbolicprog.tabbedframe.PaneProgFrame#printPanes(boolean)
878        // as user expects to see more tabs on printout using Comprehensive or just 1 tab for Basic programmer
879        pre.printPanes(preview);
880    }
881
882    /**
883     * Print the displayed table, as displayed.
884     *
885     */
886    protected void printCurrentTable() {
887        try {
888            var cal = java.util.Calendar.getInstance();
889            var sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm");
890            String time = sdf.format(cal.getTime());
891
892            var selectedRosterGroup = getSelectedRosterGroup();
893            String g = (selectedRosterGroup != null) ? selectedRosterGroup : "All Entries";
894            String group = String.format("%-20s",g);  // pad to right to fixed length
895
896            rtable.getTable().print(javax.swing.JTable.PrintMode.FIT_WIDTH,
897                            null,  // no header
898                            new java.text.MessageFormat(group+" - {0} -   "+time)  // spaces for heuristic formatting, don't change
899                            );
900        } catch (java.awt.print.PrinterException ep) {
901            log.error("While printing",ep);
902        }
903    }
904
905    /**
906     * Match the first argument in the array against a locally-known method.
907     *
908     * @param args Array of arguments, we take with element 0
909     */
910    @Override
911    public void remoteCalls(String[] args) {
912        args[0] = args[0].toLowerCase();
913        switch (args[0]) {
914            case "identifyloco":
915                startIdentifyLoco();
916                break;
917            case "printcurrenttable":
918                    printCurrentTable();
919                break;
920            case "printloco":
921                if (checkIfEntrySelected()) {
922                    printLoco(false);
923                }
924                break;
925            case "printpreviewloco":
926                if (checkIfEntrySelected()) {
927                    printLoco(true);
928                }
929                break;
930            case "exportloco":
931                if (checkIfEntrySelected()) {
932                    exportLoco();
933                }
934                break;
935            case "basicprogrammer":
936                if (checkIfEntrySelected()) {
937                    startProgrammer(null, re, programmer2);
938                }
939                break;
940            case "comprehensiveprogrammer":
941                if (checkIfEntrySelected()) {
942                    startProgrammer(null, re, programmer1);
943                }
944                break;
945            case "editthrottlelabels":
946                if (checkIfEntrySelected()) {
947                    startProgrammer(null, re, "dp3" + File.separator + "ThrottleLabels");
948                }
949                break;
950            case "editrostermedia":
951                if (checkIfEntrySelected()) {
952                    startProgrammer(null, re, "dp3" + File.separator + "MediaPane");
953                }
954                break;
955            case "hiderosterimage":
956                hideRosterImage();
957                break;
958            case "summarypane":
959                hideSummary();
960                break;
961            case "copyloco":
962                if (checkIfEntrySelected()) {
963                    copyLoco();
964                }
965                break;
966            case "deleteloco":
967                if (checkIfEntrySelected(true)) {
968                    deleteLoco();
969                }
970                break;
971            case "setprogservice":
972                service.setSelected(true);
973                break;
974            case "setprogops":
975                ops.setSelected(true);
976                break;
977            case "setprogedit":
978                edit.setSelected(true);
979                break;
980            case "groupspane":
981                hideGroups();
982                break;
983            case "quit":
984                saveWindowDetails();
985                handleQuit(new WindowEvent(this, frameInstances.size()));
986                break;
987            case "closewindow":
988                closeWindow(null);
989                break;
990            case "newwindow":
991                newWindow();
992                break;
993            case "resettablecolumns":
994                rtable.resetColumnWidths();
995                break;
996            default:
997                log.error("method {} not found", args[0]);
998                break;
999        }
1000    }
1001
1002    JPanel rosterDetails() {
1003        JPanel panel = new JPanel();
1004        GridBagLayout gbLayout = new GridBagLayout();
1005        GridBagConstraints cL = new GridBagConstraints();
1006        GridBagConstraints cR = new GridBagConstraints();
1007        Dimension minFieldDim = new Dimension(30, 20);
1008        cL.gridx = 0;
1009        cL.gridy = 0;
1010        cL.ipadx = 3;
1011        cL.anchor = GridBagConstraints.EAST;
1012        cL.insets = new Insets(0, 0, 0, 15);
1013        JLabel row0Label = new JLabel(Bundle.getMessage("FieldID") + ":", JLabel.LEFT);
1014        gbLayout.setConstraints(row0Label, cL);
1015        panel.setLayout(gbLayout);
1016        panel.add(row0Label);
1017        cR.gridx = 1;
1018        cR.gridy = 0;
1019        cR.anchor = GridBagConstraints.WEST;
1020        id.setMinimumSize(minFieldDim);
1021        gbLayout.setConstraints(id, cR);
1022        formatTextAreaAsLabel(id);
1023        panel.add(id);
1024        cL.gridy = 1;
1025        JLabel row1Label = new JLabel(Bundle.getMessage("FieldRoadName") + ":", JLabel.LEFT);
1026        gbLayout.setConstraints(row1Label, cL);
1027        panel.add(row1Label);
1028        cR.gridy = 1;
1029        roadName.setMinimumSize(minFieldDim);
1030        gbLayout.setConstraints(roadName, cR);
1031        formatTextAreaAsLabel(roadName);
1032        panel.add(roadName);
1033        cL.gridy = 2;
1034        JLabel row2Label = new JLabel(Bundle.getMessage("FieldRoadNumber") + ":");
1035        gbLayout.setConstraints(row2Label, cL);
1036        panel.add(row2Label);
1037        cR.gridy = 2;
1038        roadNumber.setMinimumSize(minFieldDim);
1039        gbLayout.setConstraints(roadNumber, cR);
1040        formatTextAreaAsLabel(roadNumber);
1041        panel.add(roadNumber);
1042        cL.gridy = 3;
1043        JLabel row3Label = new JLabel(Bundle.getMessage("FieldManufacturer") + ":");
1044        gbLayout.setConstraints(row3Label, cL);
1045        panel.add(row3Label);
1046        cR.gridy = 3;
1047        mfg.setMinimumSize(minFieldDim);
1048        gbLayout.setConstraints(mfg, cR);
1049        formatTextAreaAsLabel(mfg);
1050        panel.add(mfg);
1051        cL.gridy = 4;
1052        JLabel row4Label = new JLabel(Bundle.getMessage("FieldOwner") + ":");
1053        gbLayout.setConstraints(row4Label, cL);
1054        panel.add(row4Label);
1055        cR.gridy = 4;
1056        owner.setMinimumSize(minFieldDim);
1057        gbLayout.setConstraints(owner, cR);
1058        formatTextAreaAsLabel(owner);
1059        panel.add(owner);
1060        cL.gridy = 5;
1061        JLabel row5Label = new JLabel(Bundle.getMessage("FieldModel") + ":");
1062        gbLayout.setConstraints(row5Label, cL);
1063        panel.add(row5Label);
1064        cR.gridy = 5;
1065        model.setMinimumSize(minFieldDim);
1066        gbLayout.setConstraints(model, cR);
1067        formatTextAreaAsLabel(model);
1068        panel.add(model);
1069        cL.gridy = 6;
1070        JLabel row6Label = new JLabel(Bundle.getMessage("FieldDCCAddress") + ":");
1071        gbLayout.setConstraints(row6Label, cL);
1072        panel.add(row6Label);
1073        cR.gridy = 6;
1074        dccAddress.setMinimumSize(minFieldDim);
1075        gbLayout.setConstraints(dccAddress, cR);
1076        formatTextAreaAsLabel(dccAddress);
1077        panel.add(dccAddress);
1078        cL.gridy = 7;
1079        cR.gridy = 7;
1080        cL.gridy = 8;
1081        cR.gridy = 8;
1082        cL.gridy = 9;
1083        JLabel row9Label = new JLabel(Bundle.getMessage("FieldDecoderFamily") + ":");
1084        gbLayout.setConstraints(row9Label, cL);
1085        panel.add(row9Label);
1086        cR.gridy = 9;
1087        decoderFamily.setMinimumSize(minFieldDim);
1088        gbLayout.setConstraints(decoderFamily, cR);
1089        formatTextAreaAsLabel(decoderFamily);
1090        panel.add(decoderFamily);
1091        cL.gridy = 10;
1092        JLabel row10Label = new JLabel(Bundle.getMessage("FieldDecoderModel") + ":");
1093        gbLayout.setConstraints(row10Label, cL);
1094        panel.add(row10Label);
1095        cR.gridy = 10;
1096        decoderModel.setMinimumSize(minFieldDim);
1097        gbLayout.setConstraints(decoderModel, cR);
1098        formatTextAreaAsLabel(decoderModel);
1099        panel.add(decoderModel);
1100        cL.gridy = 11;
1101        cR.gridy = 11;
1102        cL.gridy = 12;
1103        JLabel row12Label = new JLabel(Bundle.getMessage("FieldFilename") + ":");
1104        gbLayout.setConstraints(row12Label, cL);
1105        panel.add(row12Label);
1106        cR.gridy = 12;
1107        filename.setMinimumSize(minFieldDim);
1108        gbLayout.setConstraints(filename, cR);
1109        formatTextAreaAsLabel(filename);
1110        panel.add(filename);
1111        cL.gridy = 13;
1112        /*
1113         * JLabel row13Label = new
1114         * JLabel(Bundle.getMessage("FieldDateUpdated")+":");
1115         * gbLayout.setConstraints(row13Label,cL); panel.add(row13Label);
1116         */
1117        cR.gridy = 13;
1118        /*
1119         * filename.setMinimumSize(minFieldDim);
1120         * gbLayout.setConstraints(dateUpdated,cR); panel.add(dateUpdated);
1121         */
1122        formatTextAreaAsLabel(dateUpdated);
1123        JPanel retval = new JPanel(new FlowLayout(FlowLayout.LEFT));
1124        retval.add(panel);
1125        return retval;
1126    }
1127
1128    void saveWindowDetails() {
1129        prefsMgr.setSimplePreferenceState(this.getClass().getName() + ".hideSummary", hideBottomPane);
1130        prefsMgr.setSimplePreferenceState(this.getClass().getName() + ".hideGroups", hideGroups);
1131        prefsMgr.setSimplePreferenceState(this.getClass().getName() + ".hideRosterImage", hideRosterImage);
1132        prefsMgr.setProperty(getWindowFrameRef(), SELECTED_ROSTER_GROUP, groups.getSelectedRosterGroup());
1133        String selectedProgMode = "edit";
1134        if (service.isSelected()) {
1135            selectedProgMode = "service";
1136        }
1137        if (ops.isSelected()) {
1138            selectedProgMode = "ops";
1139        }
1140        prefsMgr.setProperty(getWindowFrameRef(), "selectedProgrammer", selectedProgMode);
1141
1142        if (rosterGroupSplitPane.getDividerLocation() > 2) {
1143            prefsMgr.setProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation", rosterGroupSplitPane.getDividerLocation());
1144        } else if (groupSplitPaneLocation > 2) {
1145            prefsMgr.setProperty(getWindowFrameRef(), "rosterGroupPaneDividerLocation", groupSplitPaneLocation);
1146        }
1147    }
1148
1149    /**
1150     * Identify locomotive complete, act on it by setting the GUI. This will
1151     * fire "GUI changed" events which will reset the decoder GUI.
1152     *
1153     * @param dccAddress address of locomotive
1154     * @param isLong     true if address is long; false if short
1155     * @param mfgId      manufacturer id as in decoder
1156     * @param modelId    model id as in decoder
1157     */
1158    protected void selectLoco(int dccAddress, boolean isLong, int mfgId, int modelId) {
1159        // raise the button again
1160        // idloco.setSelected(false);
1161        // locate that loco
1162        inStartProgrammer = false;
1163        if (re != null) {
1164            //We remove the propertychangelistener if we had a previoulsy selected entry;
1165            re.removePropertyChangeListener(rosterEntryUpdateListener);
1166        }
1167        List<RosterEntry> l = Roster.getDefault().matchingList(null, null, Integer.toString(dccAddress), null, null, null, null);
1168        log.debug("selectLoco found {} matches", l.size());
1169        if (!l.isEmpty()) {
1170            if (l.size() > 1) {
1171                //More than one possible loco, so check long flag
1172                List<RosterEntry> l2 = new ArrayList<>();
1173                for (RosterEntry _re : l) {
1174                    if (_re.isLongAddress() == isLong) {
1175                        l2.add(_re);
1176                    }
1177                }
1178                if (l2.size() == 1) {
1179                    re = l2.get(0);
1180                } else {
1181                    if (l2.isEmpty()) {
1182                        l2 = l;
1183                    }
1184                    // Still more than one possible loco, so check against the decoder family
1185                    log.trace("Checking against decoder family with mfg {} model {}", mfgId, modelId);
1186                    List<RosterEntry> l3 = new ArrayList<>();
1187                    List<DecoderFile> temp = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, null, "" + mfgId, "" + modelId, null, null);
1188                    log.trace("found {}", temp.size());
1189                    ArrayList<String> decoderFam = new ArrayList<>();
1190                    for (DecoderFile f : temp) {
1191                        if (!decoderFam.contains(f.getModel())) {
1192                            decoderFam.add(f.getModel());
1193                        }
1194                    }
1195                    log.trace("matched {} times", decoderFam.size());
1196
1197                    for (RosterEntry _re : l2) {
1198                        if (decoderFam.contains(_re.getDecoderModel())) {
1199                            l3.add(_re);
1200                        }
1201                    }
1202                    if (l3.isEmpty()) {
1203                        //Unable to determine the loco against the manufacture therefore will be unable to further identify against decoder.
1204                        re = l2.get(0);
1205                    } else {
1206                        //We have no other options to match against so will return the first one we come across;
1207                        re = l3.get(0);
1208                    }
1209                }
1210            } else {
1211                re = l.get(0);
1212            }
1213            re.addPropertyChangeListener(rosterEntryUpdateListener);
1214            rtable.setSelection(re);
1215            updateDetails();
1216            rtable.moveTableViewToSelected();
1217        } else {
1218            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"
1219            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("NotFoundError", dccAddress), Bundle.getMessage("NotFoundErrorTitle", dccAddress), JmriJOptionPane.INFORMATION_MESSAGE);
1220        }
1221    }
1222
1223    /**
1224     * Simple method to change over the programmer buttons.
1225     * <p>
1226     * TODO This should be implemented with the buttons in their own class etc.
1227     * but this will work for now.
1228     *
1229     * @param buttonId   1 or 2; use 1 for basic programmer; 2 for comprehensive
1230     *                   programmer
1231     * @param programmer name of programmer
1232     * @param buttonText button title
1233     */
1234    public void setProgrammerLaunch(int buttonId, String programmer, String buttonText) {
1235        if (buttonId == 1) {
1236            programmer1 = programmer;
1237            prog1Button.setText(buttonText);
1238        } else if (buttonId == 2) {
1239            programmer2 = programmer;
1240            prog2Button.setText(buttonText);
1241        }
1242    }
1243
1244    public void setSelectedRosterGroup(String rosterGroup) {
1245        groups.setSelectedRosterGroup(rosterGroup);
1246    }
1247
1248    protected void showPopup(JmriMouseEvent e) {
1249        int row = rtable.getTable().rowAtPoint(e.getPoint());
1250        if (!rtable.getTable().isRowSelected(row)) {
1251            rtable.getTable().changeSelection(row, 0, false, false);
1252        }
1253        JPopupMenu popupMenu = new JPopupMenu();
1254
1255        JMenuItem menuItem = new JMenuItem(Bundle.getMessage("Program"));
1256        menuItem.addActionListener((ActionEvent e1) -> startProgrammer(null, re, programmer1));
1257        if (re == null) {
1258            menuItem.setEnabled(false);
1259        }
1260        popupMenu.add(menuItem);
1261        ButtonGroup group = new ButtonGroup();
1262        group.add(contextService);
1263        group.add(contextOps);
1264        group.add(contextEdit);
1265        JMenu progMenu = new JMenu(Bundle.getMessage("ProgrammerType"));
1266        contextService.addActionListener((ActionEvent e1) -> {
1267            service.setSelected(true);
1268            updateProgMode();
1269        });
1270        progMenu.add(contextService);
1271        contextOps.addActionListener((ActionEvent e1) -> {
1272            ops.setSelected(true);
1273            updateProgMode();
1274        });
1275        progMenu.add(contextOps);
1276        contextEdit.addActionListener((ActionEvent e1) -> {
1277            edit.setSelected(true);
1278            updateProgMode();
1279        });
1280        if (service.isSelected()) {
1281            contextService.setSelected(true);
1282        } else if (ops.isSelected()) {
1283            contextOps.setSelected(true);
1284        } else {
1285            contextEdit.setSelected(true);
1286        }
1287        progMenu.add(contextEdit);
1288        popupMenu.add(progMenu);
1289
1290        popupMenu.addSeparator();
1291        menuItem = new JMenuItem(Bundle.getMessage("LabelsAndMedia"));
1292        menuItem.addActionListener((ActionEvent e1) -> editMediaButton());
1293        if (re == null) {
1294            menuItem.setEnabled(false);
1295        }
1296        popupMenu.add(menuItem);
1297        menuItem = new JMenuItem(Bundle.getMessage("Throttle"));
1298        menuItem.addActionListener((ActionEvent e1) -> {
1299            ThrottleControllersUIContainer tw = null;
1300            for (RosterEntry re : rtable.getSelectedRosterEntries()) {
1301                ThrottleControllerUI tf;
1302                if (tw == null) {
1303                    tf = InstanceManager.getDefault(ThrottleFrameManager.class).createThrottleFrame();
1304                    tw = tf.getThrottleControllersContainer();
1305                } else {
1306                    tf = tw.newThrottleController();
1307                }
1308                tf.toFront();
1309                tf.setRosterEntry(re);
1310            }
1311        });
1312        popupMenu.add(menuItem);
1313        menuItem = new JMenuItem(Bundle.getMessage("SimpleThrottle"));
1314        menuItem.addActionListener((ActionEvent e1) -> {
1315            ThrottleControllerUI tf =InstanceManager.getDefault(ThrottleFrameManager.class).createSimpleThrottleFrame(re);
1316            tf.toFront();
1317        });
1318        if (re == null) {
1319            menuItem.setEnabled(false);
1320        }
1321        popupMenu.add(menuItem);
1322        popupMenu.addSeparator();
1323
1324        menuItem = new JMenuItem(Bundle.getMessage("PrintSelection"));
1325        menuItem.addActionListener((ActionEvent e1) -> printLoco(false));
1326        if (re == null) {
1327            menuItem.setEnabled(false);
1328        }
1329        popupMenu.add(menuItem);
1330        menuItem = new JMenuItem(Bundle.getMessage("PreviewSelection"));
1331        menuItem.addActionListener((ActionEvent e1) -> printLoco(true));
1332        if (re == null) {
1333            menuItem.setEnabled(false);
1334        }
1335        popupMenu.add(menuItem);
1336        popupMenu.addSeparator();
1337
1338        menuItem = new JMenuItem(Bundle.getMessage("Duplicateddd"));
1339        menuItem.addActionListener((ActionEvent e1) -> copyLoco());
1340        if (re == null) {
1341            menuItem.setEnabled(false);
1342        }
1343        popupMenu.add(menuItem);
1344        boolean deleteFromGroup = this.getSelectedRosterGroup() != null && !this.getSelectedRosterGroup().equals(Roster.NOGROUP);
1345        menuItem = new JMenuItem(deleteFromGroup ? Bundle.getMessage("DeleteFromGroup") : Bundle.getMessage("DeleteFromRoster")); // NOI18N
1346        menuItem.addActionListener((ActionEvent e1) -> deleteLoco());
1347        popupMenu.add(menuItem);
1348        menuItem.setEnabled(this.getSelectedRosterEntries().length > 0);
1349
1350        menuItem = new JMenuItem(new jmri.jmrit.roster.swing.RosterEntryToGroupAction(Bundle.getMessage("AddToGroup"), re));
1351        if (re == null) {
1352            menuItem.setEnabled(false);
1353        }
1354        popupMenu.add(menuItem);
1355
1356        popupMenu.show(e.getComponent(), e.getX(), e.getY());
1357    }
1358
1359    /**
1360     * Start the identify operation after [Identify Loco] button pressed.
1361     * <p>
1362     * This defines what happens when Identify is done.
1363     */
1364    //taken out of CombinedLocoSelPane
1365    protected void startIdentifyLoco() {
1366        final RosterFrame me = this;
1367        Programmer programmer = null;
1368        if (modePanel.isSelected()) {
1369            programmer = modePanel.getProgrammer();
1370        }
1371        if (programmer == null) {
1372            GlobalProgrammerManager gpm = InstanceManager.getNullableDefault(GlobalProgrammerManager.class);
1373            if (gpm != null) {
1374                programmer = gpm.getGlobalProgrammer();
1375                log.warn("Selector did not provide a programmer, attempt to use GlobalProgrammerManager default: {}", programmer);
1376            } else {
1377                log.warn("Selector did not provide a programmer, and no ProgramManager found in InstanceManager");
1378            }
1379        }
1380
1381        // if failed to get programmer, tell user and stop
1382        if (programmer == null) {
1383            log.error("Identify loco called when no service mode programmer is available; button should have been disabled");
1384            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("IdentifyError"));
1385            return;
1386        }
1387
1388        // and now do the work
1389        IdentifyLoco ident = new IdentifyLoco(programmer) {
1390            private final RosterFrame who = me;
1391
1392            @Override
1393            protected void done(int dccAddress) {
1394                // if Done, updated the selected decoder
1395                // on the GUI thread, right now
1396                jmri.util.ThreadingUtil.runOnGUI(() -> who.selectLoco(dccAddress, !shortAddr, cv8val, cv7val));
1397            }
1398
1399            @Override
1400            protected void message(String m) {
1401                // on the GUI thread, right now
1402                jmri.util.ThreadingUtil.runOnGUI(() -> statusField.setText(m));
1403            }
1404
1405            @Override
1406            protected void error() {
1407                // raise the button again
1408                //idloco.setSelected(false);
1409            }
1410        };
1411        ident.start();
1412    }
1413
1414    protected void startProgrammer(DecoderFile decoderFile, RosterEntry re, String filename) {
1415        if (inStartProgrammer) {
1416            log.debug("Call to start programmer has been called twice when the first call hasn't opened");
1417            return;
1418        }
1419        if (!checkIfEntrySelected()) {
1420            return;
1421        }
1422        try {
1423            setCursor(new Cursor(Cursor.WAIT_CURSOR));
1424            inStartProgrammer = true;
1425            String title = re.getId();
1426            JFrame progFrame = null;
1427            if (edit.isSelected()) {
1428                progFrame = new PaneProgFrame(decoderFile, re, title, "programmers" + File.separator + filename + ".xml", null, false) {
1429                    @Override
1430                    protected JPanel getModePane() {
1431                        return null;
1432                    } // hide prog mode buttons pane
1433                };
1434            } else if (service.isSelected()) {
1435                progFrame = new PaneServiceProgFrame(decoderFile, re, title, "programmers" + File.separator + filename + ".xml", modePanel.getProgrammer());
1436            } else if (ops.isSelected()) {
1437                int address = Integer.parseInt(re.getDccAddress());
1438                boolean longAddr = re.isLongAddress();
1439                Programmer pProg = InstanceManager.getDefault(AddressedProgrammerManager.class).getAddressedProgrammer(longAddr, address);
1440                progFrame = new PaneOpsProgFrame(decoderFile, re, title, "programmers" + File.separator + filename + ".xml", pProg);
1441            }
1442            if (progFrame == null) {
1443                return;
1444            }
1445            progFrame.pack();
1446            progFrame.setVisible(true);
1447        } finally {
1448            setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
1449        }
1450        inStartProgrammer = false;
1451    }
1452
1453    /**
1454     * Create and display a status bar along the bottom edge of the Roster main
1455     * pane.
1456     * <p>
1457     * TODO This status bar needs sorting out properly
1458     */
1459    protected void statusBar() {
1460        addToStatusBox(serviceModeProgrammerLabel, null);
1461        addToStatusBox(operationsModeProgrammerLabel, null);
1462        JLabel programmerStatusLabel = new JLabel(Bundle.getMessage("ProgrammerStatus"));
1463        statusField.setText(Bundle.getMessage("StateIdle"));
1464        addToStatusBox(programmerStatusLabel, statusField);
1465        Profile profile = ProfileManager.getDefault().getActiveProfile();
1466        if (profile != null) {
1467            addToStatusBox(new JLabel(Bundle.getMessage("ActiveProfile", profile.getName())), null);
1468        }
1469    }
1470
1471    protected void systemsMenu() {
1472        ActiveSystemsMenu.addItems(getMenu());
1473        getMenu().add(new WindowMenu(this));
1474    }
1475
1476    void updateDetails() {
1477        if (re == null) {
1478            String value = (rtable.getTable().getSelectedRowCount() > 1) ? "Multiple Items Selected" : "";
1479            filename.setText(value);
1480            dateUpdated.setText(value);
1481            decoderModel.setText(value);
1482            decoderFamily.setText(value);
1483            id.setText(value);
1484            roadName.setText(value);
1485            dccAddress.setText(value);
1486            roadNumber.setText(value);
1487            mfg.setText(value);
1488            model.setText(value);
1489            owner.setText(value);
1490            locoImage.setImagePath(null);
1491            service.setEnabled(false);
1492            ops.setEnabled(false);
1493            edit.setEnabled(false);
1494            prog1Button.setEnabled(false);
1495            prog2Button.setEnabled(false);
1496            throttleLabels.setEnabled(false);
1497            rosterMedia.setEnabled(false);
1498            throttleLaunch.setEnabled(false );
1499        } else {
1500            filename.setText(re.getFileName());
1501            dateUpdated.setText((re.getDateModified() != null)
1502                    ? DateFormat.getDateTimeInstance().format(re.getDateModified())
1503                    : re.getDateUpdated());
1504            decoderModel.setText(re.getDecoderModel());
1505            decoderFamily.setText(re.getDecoderFamily());
1506            dccAddress.setText(re.getDccAddress());
1507            id.setText(re.getId());
1508            roadName.setText(re.getRoadName());
1509            roadNumber.setText(re.getRoadNumber());
1510            mfg.setText(re.getMfg());
1511            model.setText(re.getModel());
1512            owner.setText(re.getOwner());
1513            locoImage.setImagePath(re.getImagePath());
1514            if (hideRosterImage) {
1515                locoImage.setVisible(false);
1516            } else {
1517                locoImage.setVisible(true);
1518            }
1519            service.setEnabled(isProgrammingTrackEnabled());
1520            ops.setEnabled(isProgrammingOnMainEnabled());
1521            edit.setEnabled(true);
1522            prog1Button.setEnabled(true);
1523            prog2Button.setEnabled(true);
1524            throttleLabels.setEnabled(true);
1525            rosterMedia.setEnabled(true);
1526            throttleLaunch.setEnabled(true);
1527            updateProgMode();
1528        }
1529    }
1530
1531    void updateProgMode() {
1532        String progMode;
1533        if (service.isSelected()) {
1534            progMode = "setprogservice";
1535        } else if (ops.isSelected()) {
1536            progMode = "setprogops";
1537        } else {
1538            progMode = "setprogedit";
1539        }
1540        firePropertyChange(progMode, "setSelected", true);
1541    }
1542
1543    /**
1544     * Handle setting up and updating the GUI for the types of programmer
1545     * available.
1546     *
1547     * @param evt the triggering event; if not null and if a removal of a
1548     *            ProgrammerManager, care will be taken not to trigger the
1549     *            automatic creation of a new ProgrammerManager
1550     */
1551    protected void updateProgrammerStatus(@CheckForNull PropertyChangeEvent evt) {
1552        log.debug("Updating Programmer Status");
1553        ConnectionConfig oldServMode = serModeProCon;
1554        ConnectionConfig oldOpsMode = opsModeProCon;
1555        GlobalProgrammerManager gpm = null;
1556        AddressedProgrammerManager apm = null;
1557
1558        // Find the connection that goes with the global programmer
1559        // test that IM has a default GPM, or that event is not the removal of a GPM
1560        if (InstanceManager.containsDefault(GlobalProgrammerManager.class)
1561                || (evt != null
1562                && evt.getPropertyName().equals(InstanceManager.getDefaultsPropertyName(GlobalProgrammerManager.class))
1563                && evt.getNewValue() == null)) {
1564            gpm = InstanceManager.getNullableDefault(GlobalProgrammerManager.class);
1565            log.trace("found global programming manager {}", gpm);
1566        }
1567        if (gpm != null) {
1568            String serviceModeProgrammerName = gpm.getUserName();
1569            log.debug("GlobalProgrammerManager found of class {} name {} ", gpm.getClass(), serviceModeProgrammerName);
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(serviceModeProgrammerName)) {
1574                        log.debug("Connection found for GlobalProgrammermanager");
1575                        serModeProCon = connection;
1576                    }
1577                }
1578            });
1579        }
1580
1581        // Find the connection that goes with the addressed programmer
1582        // test that IM has a default APM, or that event is not the removal of an APM
1583        if (InstanceManager.containsDefault(AddressedProgrammerManager.class)
1584                || (evt != null
1585                && evt.getPropertyName().equals(InstanceManager.getDefaultsPropertyName(AddressedProgrammerManager.class))
1586                && evt.getNewValue() == null)) {
1587            apm = InstanceManager.getNullableDefault(AddressedProgrammerManager.class);
1588            log.trace("found addressed programming manager {}", gpm);
1589        }
1590        if (apm != null) {
1591            String opsModeProgrammerName = apm.getUserName();
1592            log.debug("AddressedProgrammerManager found of class {} name {} ", apm.getClass(), opsModeProgrammerName);
1593            InstanceManager.getOptionalDefault(ConnectionConfigManager.class).ifPresent((ccm) -> {
1594                for (ConnectionConfig connection : ccm) {
1595                    log.debug("Checking connection name {}", connection.getConnectionName());
1596                    if (connection.getConnectionName() != null && connection.getConnectionName().equals(opsModeProgrammerName)) {
1597                        log.debug("Connection found for AddressedProgrammermanager");
1598                        opsModeProCon = connection;
1599                    }
1600                }
1601            });
1602        }
1603
1604        log.trace("start global check with {}, {}, {}", serModeProCon, gpm, (gpm != null ? gpm.isGlobalProgrammerAvailable() : "<none>"));
1605        if (serModeProCon != null && gpm != null && gpm.isGlobalProgrammerAvailable()) {
1606            if (ConnectionStatus.instance().isConnectionOk(serModeProCon.getAdapter().getSystemConnectionMemo())) {
1607                log.debug("GPM Connection online 1");
1608                serviceModeProgrammerLabel.setText(
1609                        Bundle.getMessage("ServiceModeProgOnline", serModeProCon.getConnectionName()));
1610                serviceModeProgrammerLabel.setForeground(new Color(0, 128, 0));
1611            } else {
1612                log.debug("GPM Connection offline");
1613                serviceModeProgrammerLabel.setText(
1614                        Bundle.getMessage("ServiceModeProgOffline", serModeProCon.getConnectionName()));
1615                serviceModeProgrammerLabel.setForeground(Color.red);
1616            }
1617            if (oldServMode == null) {
1618                log.debug("Re-enable user interface");
1619                contextService.setEnabled(isProgrammingTrackEnabled());
1620                contextService.setVisible(true);
1621                service.setEnabled(isProgrammingTrackEnabled());
1622                service.setVisible(true);
1623                firePropertyChange("setprogservice", "setEnabled", true);
1624                getToolBar().getComponents()[1].setEnabled(true);
1625            }
1626        } else if (gpm != null && gpm.isGlobalProgrammerAvailable()) {
1627            if (ConnectionStatus.instance().isSystemOk(gpm.getUserName())) {
1628                log.debug("GPM Connection online 2");
1629                serviceModeProgrammerLabel.setText(
1630                        Bundle.getMessage("ServiceModeProgOnline", gpm.getUserName()));
1631                serviceModeProgrammerLabel.setForeground(new Color(0, 128, 0));
1632            } else {
1633                log.debug("GPM Connection onffline");
1634                serviceModeProgrammerLabel.setText(
1635                        Bundle.getMessage("ServiceModeProgOffline", gpm.getUserName()));
1636                serviceModeProgrammerLabel.setForeground(Color.red);
1637            }
1638            if (oldServMode == null) {
1639                log.debug("Re-enable user interface");
1640                contextService.setEnabled(isProgrammingTrackEnabled());
1641                contextService.setVisible(true);
1642                service.setEnabled(isProgrammingTrackEnabled());
1643                service.setVisible(true);
1644                firePropertyChange("setprogservice", "setEnabled", true);
1645                getToolBar().getComponents()[1].setEnabled(true);
1646            }
1647        } else {
1648            // No service programmer available, disable interface sections not available
1649            log.debug("no service programmer");
1650            serviceModeProgrammerLabel.setText(Bundle.getMessage("NoServiceProgrammerAvailable"));
1651            serviceModeProgrammerLabel.setForeground(Color.red);
1652            if (oldServMode != null) {
1653                contextService.setEnabled(false);
1654                contextService.setVisible(false);
1655                service.setEnabled(false);
1656                service.setVisible(false);
1657                firePropertyChange("setprogservice", "setEnabled", false);
1658            }
1659            // Disable Identify in toolBar
1660            // This relies on it being the 2nd item in the toolbar, as defined in xml//config/parts/jmri/jmrit/roster/swing/RosterFrameToolBar.xml
1661            // Because of I18N, we don't look for a particular Action name here
1662            getToolBar().getComponents()[1].setEnabled(false);
1663            serModeProCon = null;
1664        }
1665
1666        if (opsModeProCon != null && apm != null && apm.isAddressedModePossible()) {
1667            if (ConnectionStatus.instance().isConnectionOk(opsModeProCon.getAdapter().getSystemConnectionMemo())) {
1668                log.debug("Ops Mode Connection online");
1669                operationsModeProgrammerLabel.setText(
1670                        Bundle.getMessage("OpsModeProgOnline", opsModeProCon.getConnectionName()));
1671                operationsModeProgrammerLabel.setForeground(new Color(0, 128, 0));
1672            } else {
1673                log.debug("Ops Mode Connection offline");
1674                operationsModeProgrammerLabel.setText(
1675                        Bundle.getMessage("OpsModeProgOffline", opsModeProCon.getConnectionName()));
1676                operationsModeProgrammerLabel.setForeground(Color.red);
1677            }
1678            if (oldOpsMode == null) {
1679                contextOps.setEnabled(isProgrammingOnMainEnabled());
1680                contextOps.setVisible(true);
1681                ops.setEnabled(isProgrammingOnMainEnabled());
1682                ops.setVisible(true);
1683                firePropertyChange("setprogops", "setEnabled", true);
1684            }
1685        } else if (apm != null && apm.isAddressedModePossible()) {
1686            if (ConnectionStatus.instance().isSystemOk(apm.getUserName())) {
1687                log.debug("Ops Mode Connection online");
1688                operationsModeProgrammerLabel.setText(
1689                        Bundle.getMessage("OpsModeProgOnline", apm.getUserName()));
1690                operationsModeProgrammerLabel.setForeground(new Color(0, 128, 0));
1691            } else {
1692                log.debug("Ops Mode Connection offline");
1693                operationsModeProgrammerLabel.setText(
1694                        Bundle.getMessage("OpsModeProgOffline", apm.getUserName()));
1695                operationsModeProgrammerLabel.setForeground(Color.red);
1696            }
1697            if (oldOpsMode == null) {
1698                contextOps.setEnabled(isProgrammingOnMainEnabled());
1699                contextOps.setVisible(true);
1700                ops.setEnabled(isProgrammingOnMainEnabled());
1701                ops.setVisible(true);
1702                firePropertyChange("setprogops", "setEnabled", true);
1703            }
1704        } else {
1705            // No ops mode programmer available, disable interface sections not available
1706            log.debug("no ops mode programmer");
1707            operationsModeProgrammerLabel.setText(Bundle.getMessage("NoOpsProgrammerAvailable"));
1708            operationsModeProgrammerLabel.setForeground(Color.red);
1709            if (oldOpsMode != null) {
1710                contextOps.setEnabled(false);
1711                contextOps.setVisible(false);
1712                ops.setEnabled(false);
1713                ops.setVisible(false);
1714                firePropertyChange("setprogops", "setEnabled", false);
1715            }
1716            opsModeProCon = null;
1717        }
1718        String strProgMode;
1719        if (service.isEnabled()) {
1720            contextService.setSelected(true);
1721            service.setSelected(true);
1722            strProgMode = "setprogservice";
1723            modePanel.setVisible(true);
1724        } else if (ops.isEnabled()) {
1725            contextOps.setSelected(true);
1726            ops.setSelected(true);
1727            strProgMode = "setprogops";
1728            modePanel.setVisible(false);
1729        } else {
1730            contextEdit.setSelected(true);
1731            edit.setSelected(true);
1732            modePanel.setVisible(false);
1733            strProgMode = "setprogedit";
1734        }
1735        firePropertyChange(strProgMode, "setSelected", true);
1736    }
1737
1738    private boolean isProgrammingTrackEnabled() {
1739        return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null &&
1740                ! InstanceManager.getDefault(ProgrammerConfigManager.class).isDisableProgrammingTrack();
1741    }
1742
1743    private boolean isProgrammingOnMainEnabled() {
1744        return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null &&
1745                ! InstanceManager.getDefault(ProgrammerConfigManager.class).isDisableProgrammingOnMain();
1746    }
1747
1748    @Override
1749    public void windowClosing(WindowEvent e) {
1750        closeWindow(e);
1751        super.windowClosing(e);
1752    }
1753
1754    /**
1755     * Displays a context (right-click) menu for a roster entry.
1756     */
1757    private class RosterPopupListener extends JmriMouseAdapter {
1758
1759        @Override
1760        public void mousePressed(JmriMouseEvent e) {
1761            if (e.isPopupTrigger()) {
1762                showPopup(e);
1763            }
1764        }
1765
1766        @Override
1767        public void mouseReleased(JmriMouseEvent e) {
1768            if (e.isPopupTrigger()) {
1769                showPopup(e);
1770            }
1771        }
1772
1773        @Override
1774        public void mouseClicked(JmriMouseEvent e) {
1775            if (e.isPopupTrigger()) {
1776                showPopup(e);
1777                return;
1778            }
1779            if (e.getClickCount() == 2) {
1780                startProgrammer(null, re, programmer1);
1781            }
1782        }
1783    }
1784
1785    private static class ExportRosterItem extends ExportRosterItemAction {
1786
1787        ExportRosterItem(String pName, Component pWho, RosterEntry re) {
1788            super(pName, pWho);
1789            super.setExistingEntry(re);
1790        }
1791
1792        @Override
1793        protected boolean selectFrom() {
1794            return true;
1795        }
1796    }
1797
1798    private static class CopyRosterItem extends CopyRosterItemAction {
1799
1800        CopyRosterItem(String pName, Component pWho, RosterEntry re) {
1801            super(pName, pWho);
1802            super.setExistingEntry(re);
1803        }
1804
1805        @Override
1806        protected boolean selectFrom() {
1807            return true;
1808        }
1809    }
1810    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterFrame.class);
1811
1812}