001package jmri.jmrit.throttle;
002
003import java.awt.*;
004import java.awt.event.*;
005import java.beans.PropertyChangeEvent;
006import java.beans.PropertyChangeListener;
007import java.io.File;
008import java.net.URI;
009import java.util.*;
010import java.util.List;
011
012import javax.annotation.CheckForNull;
013import javax.swing.*;
014
015import jmri.*;
016import jmri.jmrit.catalog.NamedIcon;
017import jmri.jmrit.jython.Jynstrument;
018import jmri.jmrit.jython.JynstrumentFactory;
019import jmri.jmrit.throttle.actions.ThrottleWindowActionsFactory;
020import jmri.jmrit.throttle.actions.ThrottleWindowInputsListener;
021import jmri.jmrit.throttle.preferences.ThrottlesPreferences;
022import jmri.jmrit.throttle.buttons.LargePowerManagerButton;
023import jmri.jmrit.throttle.buttons.SmallPowerManagerButton;
024import jmri.jmrit.throttle.buttons.StopAllButton;
025import jmri.jmrit.throttle.implementation.ThrottleFrame;
026import jmri.jmrit.throttle.implementation.ThrottleFramePropertyEditor;
027import jmri.jmrit.throttle.implementation.WindowPreferences;
028import jmri.jmrit.throttle.interfaces.ThrottleControllerUI;
029import jmri.jmrit.throttle.interfaces.ThrottleControllersUIContainer;
030import jmri.util.FileUtil;
031import jmri.util.JmriJFrame;
032import jmri.util.iharder.dnd.URIDrop;
033import jmri.util.swing.TransparencyUtils;
034
035import org.jdom2.Element;
036import org.jdom2.Attribute;
037
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041/**
042 * The JMRI throttle window.
043 * 
044 * <hr>
045 * This file is part of JMRI.
046 * <p>
047 * JMRI is free software; you can redistribute it and/or modify it under the
048 * terms of version 2 of the GNU General Public License as published by the Free
049 * Software Foundation. See the "COPYING" file for a copy of this license.
050 * <p>
051 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
052 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
053 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
054 *
055 * @author Lionel Jeanson 2007-2026
056 * 
057 */
058
059public class ThrottleWindow extends JmriJFrame implements ThrottleControllersUIContainer, PropertyChangeListener {
060
061    private final jmri.jmrix.ConnectionConfig connectionConfig;
062    private final ThrottleManager throttleManager;
063    private final ThrottleFrameManager throttleFrameManager = InstanceManager.getDefault(ThrottleFrameManager.class);
064
065    private JPanel throttlesPanel;
066    private ThrottleFrame currentThrottleFrame;
067    private CardLayout throttlesLayout;
068
069    private JCheckBoxMenuItem viewControlPanel;
070    private JCheckBoxMenuItem viewFunctionPanel;
071    private JCheckBoxMenuItem viewAddressPanel;
072    private JCheckBoxMenuItem viewSpeedPanel;
073    private JCheckBoxMenuItem viewLocoIconPanel;
074    private JCheckBoxMenuItem viewConsistFunctionsPanel;
075    private JMenuItem viewAllButtons;
076    private JMenuItem fileMenuSave;
077    private JMenuItem editMenuExportRoster;
078
079    private JButton jbPrevious = null;
080    private JButton jbNext = null;
081    private JButton jbPreviousRunning = null;
082    private JButton jbNextRunning = null;
083    private JButton jbThrottleList = null;
084    private JButton jbNew = null;
085    private JButton jbClose = null;
086    private JButton jbMode = null;
087    private JToolBar throttleToolBar;
088
089    private String titleText = "";
090    private String titleTextType = "rosterID";
091    private boolean isEditMode = true;
092
093    private final PowerManager powerMgr;
094    private SmallPowerManagerButton smallPowerMgmtButton;
095
096    private final ThrottleWindowActionsFactory myActionFactory;
097
098    private ArrayList<ThrottleFrame> throttleFrames = new ArrayList<>(5);
099
100    java.beans.PropertyChangeSupport pcs = new java.beans.PropertyChangeSupport(this);
101
102    /**
103     * Default constructor
104     */
105    public ThrottleWindow() {
106        this((jmri.jmrix.ConnectionConfig) null);
107    }
108
109    /**
110     * Constructor
111     * @param connectionConfig the connection config
112     */
113    public ThrottleWindow(jmri.jmrix.ConnectionConfig connectionConfig) {
114        super(Bundle.getMessage("ThrottleTitle"));
115        this.connectionConfig = connectionConfig;
116        if (connectionConfig != null) {
117            this.throttleManager = connectionConfig.getAdapter().getSystemConnectionMemo().get(jmri.ThrottleManager.class);
118        } else {
119            this.throttleManager = InstanceManager.getDefault(jmri.ThrottleManager.class);
120        }
121        myActionFactory = new ThrottleWindowActionsFactory(this);
122        powerMgr = InstanceManager.getNullableDefault(PowerManager.class);
123        if (powerMgr == null) {
124            log.info("No power manager instance found, panel not active");
125        }
126        pcs.addPropertyChangeListener(throttleFrameManager.getThrottlesListPanel().getTableModel());        
127        initGUI();
128        InstanceManager.getDefault(ThrottlesPreferences.class).addPropertyChangeListener(this);
129        applyPreferences();
130    }
131
132    /**
133     * Create a ThrottleWindow
134     * @param e the xml element for the throttle window
135     * @return the throttle window
136     */
137    public static ThrottleWindow createThrottleWindow(Element e) {
138        jmri.jmrix.ConnectionConfig connectionConfig = null;
139
140        Attribute systemPrefixAttr = e.getAttribute("systemPrefix");
141        if (systemPrefixAttr != null) {
142            String systemPrefix = systemPrefixAttr.getValue();
143            // Set connectionConfig to null in case the systemPrefix
144            // points to a connection that doesn't exist anymore.
145
146            for (jmri.jmrix.ConnectionConfig c : InstanceManager.getDefault(jmri.jmrix.ConnectionConfigManager.class)) {
147                if (c.getAdapter().getSystemPrefix().equals(systemPrefix)) {
148                    connectionConfig = c;
149                }
150            }
151        }
152
153        ThrottleWindow tw = new ThrottleWindow(connectionConfig);
154        tw.setXml(e);
155        return tw;
156    }
157
158    private void initGUI() {
159        setTitle(Bundle.getMessage("ThrottleTitle"));
160        setLayout(new BorderLayout());
161        throttlesLayout = new CardLayout();
162        throttlesPanel = new JPanel(throttlesLayout);
163        throttlesPanel.setDoubleBuffered(true);
164
165        initializeToolbar();
166        initializeMenu();
167
168        String txt = "ThrottleJDesktopPane-" + throttleFrameManager.generateUniqueFrameID();
169        setCurrentThrottleFrame(new ThrottleFrame(this, throttleManager));
170        getCurentThrottleController().setTitle(txt);
171        throttlesPanel.add(txt, getCurentThrottleController());
172        throttleFrames.add(getCurentThrottleController());
173        add(throttlesPanel, BorderLayout.CENTER);
174
175        installInputsListenerOnAllComponents(this);
176        // to get something to put focus on
177        getRootPane().setFocusable(true);
178
179        ActionMap am = myActionFactory.buildActionMap();
180        for (Object k : am.allKeys()) {
181            getRootPane().getActionMap().put(k, am.get(k));
182        }
183        
184        addMouseWheelListener( new ThrottleWindowInputsListener(this) );
185
186        this.addWindowListener(new WindowAdapter() {
187            @Override
188            public void windowOpened(WindowEvent e) {
189              // on initial open, force selection of address panel  
190              getCurentThrottleController().forceAddressPanelSelected();
191            }
192        });
193        updateGUI();
194    }
195
196    public void updateGUI() {
197        if (getCurentThrottleController() == null) {
198            return;
199        }
200        // title bar
201        getCurentThrottleController().updateFrameTitle();
202        // menu items
203        viewAddressPanel.setEnabled(isEditMode);
204        viewControlPanel.setEnabled(isEditMode);
205        viewFunctionPanel.setEnabled(isEditMode);
206        viewSpeedPanel.setEnabled(isEditMode);
207        viewLocoIconPanel.setEnabled(isEditMode);
208        viewConsistFunctionsPanel.setEnabled(isEditMode);
209        if (isEditMode) {
210            viewAddressPanel.setSelected(getCurentThrottleController().isAddressPanelVisible());
211            viewControlPanel.setSelected(getCurentThrottleController().isControlPanelVisible());
212            viewFunctionPanel.setSelected(getCurentThrottleController().isFunctionPanelVisible());
213            viewSpeedPanel.setSelected(getCurentThrottleController().isSpeedPanelVisible());
214            viewLocoIconPanel.setSelected(getCurentThrottleController().isLocoIconPanelVisible());
215            viewConsistFunctionsPanel.setSelected(getCurentThrottleController().isConsistFunctionsPanelVisible());
216        }
217        fileMenuSave.setEnabled(getCurentThrottleController().isKnownLastUsedSaveFile() || getCurentThrottleController().isKnownRosterEntry());
218        editMenuExportRoster.setEnabled(getCurentThrottleController().isKnownRosterEntry());
219        // toolbar items
220        if (jbPrevious != null) // means toolbar enabled
221        {
222            if (throttleFrames.size() > 1) {
223                jbPrevious.setEnabled(true);
224                jbNext.setEnabled(true);
225                jbClose.setEnabled(true);
226                jbPreviousRunning.setEnabled(true);
227                jbNextRunning.setEnabled(true);
228            } else {
229                jbPrevious.setEnabled(false);
230                jbNext.setEnabled(false);
231                jbClose.setEnabled(false);
232                jbPreviousRunning.setEnabled(false);
233                jbNextRunning.setEnabled(false);
234            }
235        }
236        getRootPane().requestFocusInWindow();
237    }
238
239    private void initializeToolbar() {
240        throttleToolBar = new JToolBar("Throttles toolbar");
241
242        jbNew = new JButton();
243        jbNew.setIcon(new NamedIcon("resources/icons/throttles/add.png", "resources/icons/throttles/add.png"));
244        jbNew.setToolTipText(Bundle.getMessage("ThrottleToolBarNewToolTip"));
245        jbNew.setVerticalTextPosition(JButton.BOTTOM);
246        jbNew.setHorizontalTextPosition(JButton.CENTER);
247        jbNew.addActionListener(e -> newThrottleController());
248        throttleToolBar.add(jbNew);
249
250        jbClose = new JButton();
251        jbClose.setIcon(new NamedIcon("resources/icons/throttles/remove.png", "resources/icons/throttles/remove.png"));
252        jbClose.setToolTipText(Bundle.getMessage("ThrottleToolBarCloseToolTip"));
253        jbClose.setVerticalTextPosition(JButton.BOTTOM);
254        jbClose.setHorizontalTextPosition(JButton.CENTER);
255        jbClose.addActionListener(e -> removeAndDisposeCurentThrottleFrame());
256        throttleToolBar.add(jbClose);
257
258        throttleToolBar.addSeparator();
259
260        jbPreviousRunning = new JButton();
261        jbPreviousRunning.setIcon(new NamedIcon("resources/icons/throttles/previous-jump.png", "resources/icons/throttles/previous-jump.png"));
262        jbPreviousRunning.setVerticalTextPosition(JButton.BOTTOM);
263        jbPreviousRunning.setHorizontalTextPosition(JButton.CENTER);
264        jbPreviousRunning.setToolTipText(Bundle.getMessage("ThrottleToolBarPrevRunToolTip"));
265        jbPreviousRunning.addActionListener(e -> previousRunningThrottleFrame());
266        throttleToolBar.add(jbPreviousRunning);
267
268        jbPrevious = new JButton();
269        jbPrevious.setIcon(new NamedIcon("resources/icons/throttles/previous.png", "resources/icons/throttles/previous.png"));
270        jbPrevious.setVerticalTextPosition(JButton.BOTTOM);
271        jbPrevious.setHorizontalTextPosition(JButton.CENTER);
272        jbPrevious.setToolTipText(Bundle.getMessage("ThrottleToolBarPrevToolTip"));
273        jbPrevious.addActionListener(e -> previousThrottleFrame());
274        throttleToolBar.add(jbPrevious);
275
276        jbNext = new JButton();
277        jbNext.setIcon(new NamedIcon("resources/icons/throttles/next.png", "resources/icons/throttles/next.png"));
278        jbNext.setToolTipText(Bundle.getMessage("ThrottleToolBarNextToolTip"));
279        jbNext.setVerticalTextPosition(JButton.BOTTOM);
280        jbNext.setHorizontalTextPosition(JButton.CENTER);
281        jbNext.addActionListener(e -> nextThrottleFrame());
282        throttleToolBar.add(jbNext);
283
284        jbNextRunning = new JButton();
285        jbNextRunning.setIcon(new NamedIcon("resources/icons/throttles/next-jump.png", "resources/icons/throttles/next-jump.png"));
286        jbNextRunning.setToolTipText(Bundle.getMessage("ThrottleToolBarNextRunToolTip"));
287        jbNextRunning.setVerticalTextPosition(JButton.BOTTOM);
288        jbNextRunning.setHorizontalTextPosition(JButton.CENTER);
289        jbNextRunning.addActionListener(e -> nextRunningThrottleFrame());
290        throttleToolBar.add(jbNextRunning);
291
292        throttleToolBar.addSeparator();
293
294        throttleToolBar.add(new StopAllButton());
295
296        if (powerMgr != null) {
297            throttleToolBar.add(new LargePowerManagerButton(false));
298        }
299
300        throttleToolBar.addSeparator();
301
302        jbMode = new JButton();
303        jbMode.setIcon(new NamedIcon("resources/icons/throttles/edit-view.png", "resources/icons/throttles/edit-view.png"));
304        jbMode.setToolTipText(Bundle.getMessage("ThrottleToolBarEditToolTip"));
305        jbMode.setVerticalTextPosition(JButton.BOTTOM);
306        jbMode.setHorizontalTextPosition(JButton.CENTER);
307        jbMode.addActionListener(e -> setEditMode( !isEditMode ));
308        throttleToolBar.add(jbMode);
309
310        throttleToolBar.addSeparator();
311
312        jbThrottleList = new JButton();
313        jbThrottleList.setIcon(new NamedIcon("resources/icons/throttles/list.png", "resources/icons/throttles/list.png"));
314        jbThrottleList.setToolTipText(Bundle.getMessage("ThrottleToolBarOpenThrottleListToolTip"));
315        jbThrottleList.setVerticalTextPosition(JButton.BOTTOM);
316        jbThrottleList.setHorizontalTextPosition(JButton.CENTER);
317        jbThrottleList.addActionListener(new ThrottlesListAction());
318        throttleToolBar.add(jbThrottleList);
319
320        // Receptacle for Jynstruments
321        new URIDrop(throttleToolBar, uris -> {
322                for (URI uri : uris ) {
323                    ynstrument(new File(uri).getPath());
324                }
325            });
326
327        add(throttleToolBar, BorderLayout.PAGE_START);
328    }
329    
330    /**
331     * Return the number of active thottle frames.
332     *
333     * @return the number of active thottle frames.
334     */
335    @Override
336    public  int getNbThrottlesControllers() {
337        return throttleFrames.size() ;
338    }
339        
340    /**
341     * Return the nth thottle frame of that throttle window
342     *
343     * @param n index of thr throtle frame
344     * @return the nth thottle frame of that throttle window
345     */
346    @Override
347    public ThrottleControllerUI getThrottleControllerAt(int n) {
348        if (! (n < throttleFrames.size())) {
349            return null;
350        }
351        return throttleFrames.get(n);
352    }
353    
354    /**
355     * Get the number of usages of a particular Loco Address.
356     * @param la the Loco Address, can be null.
357     * @return 0 if no usages, else number of AddressPanel usages.
358     */
359    @Override
360    public int getNumberOfEntriesFor(@CheckForNull DccLocoAddress la) {
361        if (la == null) { 
362            return 0; 
363        }
364        int ret = 0;
365        for (ThrottleFrame tf: throttleFrames) {
366            if (tf.isUsingAddress(la)) {
367                ret++; // in use, increment count.
368            }
369        }
370        return ret;
371    }
372       
373    @Override
374    public void emergencyStopAll() {
375        if (!throttleFrames.isEmpty()) {
376            for (ThrottleFrame tf: throttleFrames) {
377                tf.eStop();
378            }
379        }
380    }
381
382    /** {@inheritDoc} */
383    @Override
384    public void setTitle(String title) {
385        if (connectionConfig != null) {
386            super.setTitle(Bundle.getMessage("ThrottleTitleWithConnection", title, connectionConfig.getConnectionName()));
387        } else {
388            super.setTitle(title);
389        }
390    }
391
392    public void setEditMode(boolean mode) {
393        if (mode == isEditMode)
394            return;
395        isEditMode = mode;
396        if (!throttleFrames.isEmpty()) {
397            for (ThrottleFrame tf: throttleFrames) {
398                tf.setEditMode(isEditMode);
399            }
400        }
401        updateGUI();
402    }
403
404    public boolean isEditMode() {
405        return isEditMode;
406    }
407
408    public Jynstrument ynstrument(String path) {
409        Jynstrument it = JynstrumentFactory.createInstrument(path, this);
410        if (it == null) {
411            log.error("Error while creating Jynstrument {}", path);
412            return null;
413        }
414        TransparencyUtils.setOpacityRec(it, true);
415        it.setVisible(true);
416        throttleToolBar.add(it);
417        throttleToolBar.repaint();
418        return it;
419    }
420
421    /**
422     * Set up View, Edit and Power Menus
423     */
424    private void initializeMenu() {
425        JMenu fileMenu = new JMenu(Bundle.getMessage("MenuFile"));
426
427        JMenuItem fileMenuLoad = new JMenuItem(Bundle.getMessage("ThrottleFileMenuLoadThrottle"));
428        fileMenuLoad.addActionListener(new AbstractAction() {
429
430            @Override
431            public void actionPerformed(ActionEvent e) {
432                getCurentThrottleController().loadThrottleFile(null);
433            }
434        });
435        fileMenuSave = new JMenuItem(Bundle.getMessage("ThrottleFileMenuSaveThrottle"));
436        fileMenuSave.addActionListener(new AbstractAction() {
437
438            @Override
439            public void actionPerformed(ActionEvent e) {
440                getCurentThrottleController().saveThrottle();
441            }
442        });
443        JMenuItem fileMenuSaveAs = new JMenuItem(Bundle.getMessage("ThrottleFileMenuSaveAsThrottle"));
444        fileMenuSaveAs.addActionListener(new AbstractAction() {
445
446            @Override
447            public void actionPerformed(ActionEvent e) {
448                getCurentThrottleController().saveThrottleAs();
449            }
450        });
451
452        jmri.jmrit.throttle.ThrottleCreationAction.addNewThrottleItemsToThrottleMenu(fileMenu);
453        fileMenu.add(fileMenuLoad);
454        fileMenu.add(fileMenuSave);
455        fileMenu.add(fileMenuSaveAs);
456        fileMenu.addSeparator();
457
458        fileMenu.add(new jmri.jmrit.throttle.LoadXmlThrottlesLayoutAction(Bundle.getMessage("MenuItemLoadThrottleLayout")));
459        fileMenu.add(new jmri.jmrit.throttle.StoreXmlThrottlesLayoutAction(Bundle.getMessage("MenuItemSaveThrottleLayout")));
460        fileMenu.addSeparator();
461        fileMenu.add(new jmri.jmrit.throttle.LoadDefaultXmlThrottlesLayoutAction(Bundle.getMessage("MenuItemLoadDefaultThrottleLayout")));
462        fileMenu.add(new jmri.jmrit.throttle.StoreDefaultXmlThrottlesLayoutAction(Bundle.getMessage("MenuItemSaveAsDefaultThrottleLayout")));
463        fileMenu.addSeparator();
464        fileMenu.add(new jmri.jmrit.withrottle.WiThrottleCreationAction(Bundle.getMessage("MenuItemStartWiThrottle")));
465
466        JMenu viewMenu = new JMenu(Bundle.getMessage("ThrottleMenuView"));
467        viewAddressPanel = new JCheckBoxMenuItem(Bundle.getMessage("ThrottleMenuViewAddressPanel"));
468        viewAddressPanel.setSelected(true);
469        viewAddressPanel.addItemListener(e -> getCurentThrottleController().setAddressPanelVisible(e.getStateChange() == ItemEvent.SELECTED));
470
471        viewControlPanel = new JCheckBoxMenuItem(Bundle.getMessage("ThrottleMenuViewControlPanel"));
472        viewControlPanel.setSelected(true);
473        viewControlPanel.addItemListener(e -> getCurentThrottleController().setControlPanelVisible(e.getStateChange() == ItemEvent.SELECTED));
474        viewFunctionPanel = new JCheckBoxMenuItem(Bundle.getMessage("ThrottleMenuViewFunctionPanel"));
475        viewFunctionPanel.setSelected(true);
476        viewFunctionPanel.addItemListener(e -> getCurentThrottleController().setFunctionPanelVisible(e.getStateChange() == ItemEvent.SELECTED));
477        viewSpeedPanel = new JCheckBoxMenuItem(Bundle.getMessage("ThrottleMenuViewSpeedPanel"));
478        viewSpeedPanel.setSelected(false);
479        viewSpeedPanel.addItemListener(e -> getCurentThrottleController().setSpeedPanelVisible(e.getStateChange() == ItemEvent.SELECTED));
480        viewLocoIconPanel = new JCheckBoxMenuItem(Bundle.getMessage("ThrottleMenuViewLocoIconPanel"));
481        viewLocoIconPanel.setSelected(false);
482        viewLocoIconPanel.addItemListener(e -> getCurentThrottleController().setLocoIconPanelVisible(e.getStateChange() == ItemEvent.SELECTED));
483        viewConsistFunctionsPanel = new JCheckBoxMenuItem(Bundle.getMessage("ThrottleMenuViewConsistFunctionsPanel"));
484        viewConsistFunctionsPanel.setSelected(false);
485        viewConsistFunctionsPanel.addItemListener(e -> getCurentThrottleController().setConsistFunctionsPanelVisible(e.getStateChange() == ItemEvent.SELECTED));
486
487        viewAllButtons = new JMenuItem(Bundle.getMessage("ThrottleMenuViewAllFunctionButtons"));
488        viewAllButtons.addActionListener(new AbstractAction() {
489
490            @Override
491            public void actionPerformed(ActionEvent ev) {
492                getCurentThrottleController().resetFunctionPanelButton();
493            }
494        });
495
496        JMenuItem makeAllComponentsInBounds = new JMenuItem(Bundle.getMessage("ThrottleMenuViewMakeAllComponentsInBounds"));
497        makeAllComponentsInBounds.addActionListener(new AbstractAction() {
498
499            @Override
500            public void actionPerformed(ActionEvent ev) {
501                getCurentThrottleController().makeAllComponentsInBounds();
502            }
503        });
504
505        JMenuItem switchViewMode = new JMenuItem(Bundle.getMessage("ThrottleMenuViewSwitchMode"));
506        switchViewMode.addActionListener(new AbstractAction() {
507
508            @Override
509            public void actionPerformed(ActionEvent ev) {
510                setEditMode(!isEditMode);
511            }
512        });
513        JMenuItem viewThrottlesList = new JMenuItem(Bundle.getMessage("ThrottleMenuViewViewThrottleList"));
514        viewThrottlesList.addActionListener(new ThrottlesListAction());
515
516        viewMenu.add(viewAddressPanel);
517        viewMenu.add(viewControlPanel);
518        viewMenu.add(viewFunctionPanel);
519        viewMenu.add(viewSpeedPanel);
520        viewMenu.add(viewLocoIconPanel);
521        viewMenu.add(viewConsistFunctionsPanel);
522        viewMenu.addSeparator();
523        viewMenu.add(viewAllButtons);
524        viewMenu.add(makeAllComponentsInBounds);
525        viewMenu.addSeparator();
526        viewMenu.add(switchViewMode);
527        viewMenu.add(viewThrottlesList);
528
529        JMenu editMenu = new JMenu(Bundle.getMessage("MenuEdit"));
530        JMenuItem preferencesItem = new JMenuItem(Bundle.getMessage("ThrottleMenuEditFrameProperties"));
531        editMenu.add(preferencesItem);
532        preferencesItem.addActionListener(e -> editPreferences());
533        editMenuExportRoster = new JMenuItem(Bundle.getMessage("ThrottleMenuEditSaveCustoms"));
534        editMenu.add(editMenuExportRoster);
535        editMenuExportRoster.addActionListener(e -> getCurentThrottleController().saveRosterChanges());
536        editMenu.addSeparator();
537        editMenu.add(new jmri.jmrit.throttle.ThrottlesPreferencesAction(Bundle.getMessage("MenuItemThrottlesPreferences"))); // now in tabbed preferences
538
539        this.setJMenuBar(new JMenuBar());
540        this.getJMenuBar().add(fileMenu);
541        this.getJMenuBar().add(editMenu);
542        this.getJMenuBar().add(viewMenu);
543
544        if (powerMgr != null) {
545            JMenu powerMenu = new JMenu(Bundle.getMessage("ThrottleMenuPower"));
546            JMenuItem powerOn = new JMenuItem(Bundle.getMessage("ThrottleMenuPowerOn"));
547            powerMenu.add(powerOn);
548            powerOn.addActionListener(e -> {
549                try {
550                    powerMgr.setPower(PowerManager.ON);
551                } catch (JmriException e1) {
552                    log.error("Error when setting power: ", e1);
553                }
554            });
555
556            JMenuItem powerOff = new JMenuItem(Bundle.getMessage("ThrottleMenuPowerOff"));
557            powerMenu.add(powerOff);
558            powerOff.addActionListener(e -> {
559                try {
560                    powerMgr.setPower(PowerManager.OFF);
561                } catch (JmriException e1) {
562                    log.error("Error when setting power: ", e1);
563                }
564            });
565
566            this.getJMenuBar().add(powerMenu);
567
568            smallPowerMgmtButton = new SmallPowerManagerButton();
569            this.getJMenuBar().add(smallPowerMgmtButton);
570        }
571
572        // add help selection
573        addHelpMenu("package.jmri.jmrit.throttle.ThrottleFrame", true);
574    }
575
576    private void editPreferences() {
577        ThrottleFramePropertyEditor editor = new ThrottleFramePropertyEditor(this);
578        editor.setVisible(true);
579    }
580
581    /**
582     * Handle my own destruction.
583     * <ol>
584     * <li> dispose of sub windows.
585     * <li> notify my manager of my demise.
586     * </ol>
587     *
588     */
589    @Override
590    public void dispose() {        
591        InstanceManager.getDefault(ThrottlesPreferences.class).removePropertyChangeListener(this);
592        if (throttleToolBar != null) {
593            URIDrop.remove(throttleToolBar);
594            Component[] cmps = throttleToolBar.getComponents();
595            if (cmps != null) {
596                for (Component cmp : cmps) {
597                    if (cmp instanceof Jynstrument) {
598                        ((Jynstrument) cmp).exit();
599                    }
600                }
601            }
602        }
603        if ((throttleFrames != null) && (!throttleFrames.isEmpty())) {
604            for (ThrottleFrame tf: throttleFrames) {
605                tf.dispose();
606            }
607            throttleFrames.clear();
608        }
609        throttleFrames = null;
610        currentThrottleFrame  = null;
611        for (PropertyChangeListener pcl : pcs.getPropertyChangeListeners()) {
612            pcs.removePropertyChangeListener(pcl);
613        }
614        for (MouseWheelListener mwl : getMouseWheelListeners()) {
615            removeMouseWheelListener(mwl);
616        }
617        getRootPane().getActionMap().clear();
618        throttlesPanel.removeAll();        
619        removeAll();
620        throttleFrameManager.requestThrottleWindowDestruction(this);
621        super.dispose();
622    }
623
624    public JCheckBoxMenuItem getViewControlPanel() {
625        return viewControlPanel;
626    }
627
628    public JCheckBoxMenuItem getViewFunctionPanel() {
629        return viewFunctionPanel;
630    }
631
632    public JCheckBoxMenuItem getViewAddressPanel() {
633        return viewAddressPanel;
634    }
635
636    public JCheckBoxMenuItem getViewSpeedPanel() {
637        return viewSpeedPanel;
638    }
639
640    public JCheckBoxMenuItem getViewLocoIconPanel() {
641        return viewLocoIconPanel;
642    }
643
644    public JCheckBoxMenuItem getViewConsistFunctionsPanel() {
645        return viewConsistFunctionsPanel;
646    }
647    
648    private void updateCurentThrottleFrame() {
649        currentThrottleFrame = null;
650        for (Component comp : throttlesPanel.getComponents()) {
651            if (comp instanceof ThrottleFrame && comp.isVisible()) {
652                currentThrottleFrame = (ThrottleFrame) comp;
653            }
654        }
655    }
656
657    @Override
658    public ThrottleFrame getCurentThrottleController() {
659        return currentThrottleFrame;
660    }
661
662    public void setCurrentThrottleFrame(ThrottleFrame tf) {
663        pcs.firePropertyChange("ThrottleFrameChanged", getCurentThrottleController(), tf);
664        currentThrottleFrame = tf;
665    }
666
667    @Override
668    public void removeThrottleController(ThrottleControllerUI tf) {
669        if (!(tf instanceof ThrottleFrame)) {
670            throw new IllegalArgumentException("Only ThrottleFrame can be removed from ThrottleWindow");
671        }
672        if (getCurentThrottleController() == tf) {
673            log.debug("Closing currently active throttle frame");
674            throttlesLayout.previous(throttlesPanel);
675        }
676        throttlesPanel.remove((ThrottleFrame)tf);
677        throttleFrames.remove(tf);
678        throttlesLayout.invalidateLayout(throttlesPanel);
679        updateGUI();
680        updateCurentThrottleFrame();
681        pcs.firePropertyChange("ThrottleFrameRemoved", tf, getCurentThrottleController());
682    }
683
684    /**
685     * Set next throttle frame as current frame. If the current frame is the only one, then do nothing.
686     * 
687     */
688    public void nextThrottleFrame() {
689        ThrottleFrame otf = getCurentThrottleController();
690        throttlesLayout.next(throttlesPanel);
691        updateCurentThrottleFrame();
692        updateGUI();
693        pcs.firePropertyChange("ThrottleFrameChanged", otf, getCurentThrottleController());
694    }
695
696    /**
697     * Set previous throttle frame as current frame. If the current frame is the only one, then do nothing.
698     * 
699     */
700    public void previousThrottleFrame() {
701        ThrottleFrame otf = getCurentThrottleController();
702        throttlesLayout.previous(throttlesPanel);
703        updateCurentThrottleFrame();
704        updateGUI();
705        pcs.firePropertyChange("ThrottleFrameChanged", otf, getCurentThrottleController());
706    }
707
708
709    /**
710     * Set next running (with non null speed) throttle frame as current frame. If the current frame is the only one running, then do nothing.
711     *
712     */
713    public void nextRunningThrottleFrame() {
714        if (!throttleFrames.isEmpty()) {
715            ThrottleFrame cf = this.getCurentThrottleController();
716            ThrottleFrame nf = null;
717            boolean passed = false;
718            for (ThrottleFrame tf : throttleFrames) {
719                if (tf != cf) {
720                    if (tf.isRunning()) {
721                        if (passed) { // if we passed the curent one, and found something then return it
722                            nf = tf;
723                            break;
724                        } else if (nf == null) {
725                            nf = tf;
726                        }
727                    }
728                } else {
729                    passed = true;
730                }
731            }
732            if (nf != null) {
733                nf.toFront();
734                updateCurentThrottleFrame();
735                pcs.firePropertyChange("ThrottleFrameChanged", cf, nf);
736            }
737        }
738    }
739
740    /**
741     * Set previous running (with non null speed) throttle frame as current frame. If the current frame is the only one running, then do nothing.
742     *
743     */
744    public void previousRunningThrottleFrame() {
745        if (!throttleFrames.isEmpty()) {
746            ThrottleFrame cf = this.getCurentThrottleController();
747            ThrottleFrame nf = null;            
748            for (ThrottleFrame tf : throttleFrames) {
749                if (tf.isRunning()) {
750                    nf = tf;
751                }
752                if ((tf == cf) && (nf != null)) { // return the last one found before the curent one
753                    break;
754                }
755            }
756            if (nf != null) {
757                nf.toFront();
758                updateCurentThrottleFrame();
759                pcs.firePropertyChange("ThrottleFrameChanged", cf, nf);
760            }
761        }
762    }
763
764    /**
765     * Set next throttle frame with active function (at least one function ON) or non null speed as current frame. If the current frame is the only one with active function, then do nothing.
766     * 
767     */
768    public void nextThrottleFrameWithActiveFunction() {
769        if (!throttleFrames.isEmpty()) {
770            ThrottleFrame cf = this.getCurentThrottleController();
771            ThrottleFrame nf = null;
772            boolean passed = false;
773            for (ThrottleFrame tf : throttleFrames) {
774                if (tf != cf) {
775                    if (tf.isActive()) {
776                        if (passed) { // if we passed the curent one, and found something then return it
777                            nf = tf;
778                            break;
779                        } else if (nf == null) {
780                            nf = tf;
781                        }
782                    }
783                } else {
784                    passed = true;
785                }
786            }
787            if (nf != null) {
788                nf.toFront();
789                updateCurentThrottleFrame();
790                pcs.firePropertyChange("ThrottleFrameChanged", cf, nf);
791            }
792        }
793    }
794
795    /**
796     * Set previous throttle frame with active function (at least one function ON) or non null speed as current frame. If the current frame is the only one with active function, then do nothing.
797     * 
798     */
799    public void previousThrottleFrameWithActiveFunction() {
800        if (!throttleFrames.isEmpty()) {
801            ThrottleFrame cf = this.getCurentThrottleController();
802            ThrottleFrame nf = null;            
803            for (ThrottleFrame tf : throttleFrames) {
804                if ((tf != cf) && tf.isActive()) {
805                    nf = tf;
806                }
807                if ((tf == cf) && (nf != null)) { // return the last one found before the curent one
808                    break;
809                }
810            }
811            if (nf != null) {
812                nf.toFront();
813                updateCurentThrottleFrame();
814                pcs.firePropertyChange("ThrottleFrameChanged", cf, nf);
815            }
816        }
817    }
818    
819    private void removeAndDisposeCurentThrottleFrame() {
820        ThrottleFrame tf = getCurentThrottleController();
821        removeThrottleController(getCurentThrottleController());
822        tf.dispose();
823    }
824
825    @Override
826    public void addThrottleControllerAt(ThrottleControllerUI tp, int idx) {
827        if (!(tp instanceof ThrottleFrame)) {
828            throw new IllegalArgumentException("Only ThrottleFrame supported in ThrottleWindow");
829        }
830
831        String txt = "ThrottleJDesktopPane-" + throttleFrameManager.generateUniqueFrameID();
832        ((ThrottleFrame)tp).setTitle(txt);
833        if (idx>throttleFrames.size()) {
834            idx = throttleFrames.size();
835        }
836        throttleFrames.add(idx,(ThrottleFrame)tp);
837        throttlesPanel.add((ThrottleFrame)tp,txt,idx);
838        ((ThrottleFrame)tp).setEditMode(isEditMode); // sync with window     
839        updateGUI();
840        pcs.firePropertyChange("ThrottleFrameAdded", null, this);
841    }
842
843    @Override
844    public ThrottleControllerUI newThrottleController() {
845        ThrottleFrame otf = getCurentThrottleController();
846        ThrottleFrame tf = new ThrottleFrame(this, throttleManager);
847        throttleFrames.add(tf);
848        String txt = "ThrottleJDesktopPane-" + throttleFrameManager.generateUniqueFrameID();
849        tf.setTitle(txt);        
850        throttlesPanel.add(tf, txt);  
851        tf.setEditMode(isEditMode); // sync with window                
852        installInputsListenerOnAllComponents(tf);
853        throttlesLayout.show(throttlesPanel, txt);
854        setCurrentThrottleFrame(tf);
855        updateGUI();
856        pcs.firePropertyChange("ThrottleFrameNew", otf, getCurentThrottleController());
857        return getCurentThrottleController();
858    }
859
860    public void toFront(String throttleFrameTitle) {
861        ThrottleFrame otf = getCurentThrottleController();
862        throttlesLayout.show(throttlesPanel, throttleFrameTitle);
863        updateCurentThrottleFrame();
864        setVisible(true);
865        requestFocus();
866        toFront();
867        pcs.firePropertyChange("ThrottleFrameChanged", otf, getCurentThrottleController());
868    }
869
870    public String getTitleTextType() {
871        return titleTextType;
872    }
873
874    public String getTitleText() {
875        return titleText;
876    }
877
878    public void setTitleText(String titleText) {
879        this.titleText = titleText;
880    }
881
882    public void setTitleTextType(String titleTextType) {
883        this.titleTextType = titleTextType;
884    }
885
886    public Element getXml() {
887        Element me = new Element("ThrottleWindow");
888        if (connectionConfig != null) {
889            me.setAttribute("systemPrefix", connectionConfig.getAdapter().getSystemPrefix());
890        }
891        me.setAttribute("title", titleText);
892        me.setAttribute("titleType", titleTextType);
893        me.setAttribute("isEditMode",  String.valueOf(isEditMode));
894
895        java.util.ArrayList<Element> children = new java.util.ArrayList<>(1);
896        children.add(WindowPreferences.getPreferences(this));
897        if (!throttleFrames.isEmpty()) {
898            ThrottleFrame cf = this.getCurentThrottleController();
899            for (ThrottleFrame tf : throttleFrames) {
900                if ((InstanceManager.getDefault(ThrottlesPreferences.class).isUsingExThrottle()) && (InstanceManager.getDefault(ThrottlesPreferences.class).isSavingThrottleOnLayoutSave())) {
901                    tf.toFront();
902                    tf.saveThrottle();
903                }
904                Element tfe = tf.getXmlFile();
905                if (tfe == null) {
906                    tfe = tf.getXml();
907                }
908                children.add(tfe);
909            }
910            if (cf != null) {
911                cf.toFront();
912            }
913        }
914
915        // Save Jynstruments
916        if (throttleToolBar != null) {
917            Component[] cmps = throttleToolBar.getComponents();
918            if (cmps != null) {
919                for (Component cmp : cmps) {
920                    try {
921                        if (cmp instanceof Jynstrument) {
922                            Jynstrument jyn = (Jynstrument) cmp;
923                            Element elt = new Element("Jynstrument");
924                            elt.setAttribute("JynstrumentFolder", FileUtil.getPortableFilename(jyn.getFolder()));
925                            Element je = jyn.getXml();
926                            if (je != null) {
927                                java.util.ArrayList<Element> jychildren = new java.util.ArrayList<>(1);
928                                jychildren.add(je);
929                                elt.setContent(jychildren);
930                            }
931                            children.add(elt);
932                        }
933
934                    } catch (Exception ex) {
935                        log.debug("Got exception (no panic): ", ex);
936                    }
937                }
938            }
939        }
940        me.setContent(children);
941        return me;
942    }
943
944    private void setXml(Element e) {
945        if (e.getAttribute("title") != null) {
946            setTitle(e.getAttribute("title").getValue());
947        }
948        if (e.getAttribute("title") != null) {
949            setTitleText(e.getAttribute("title").getValue());
950        }
951        if (e.getAttribute("titleType") != null) {
952            setTitleTextType(e.getAttribute("titleType").getValue());
953        }
954        if (e.getAttribute("isEditMode") != null) {
955            isEditMode = Boolean.parseBoolean(e.getAttribute("isEditMode").getValue());
956        }
957
958        Element window = e.getChild("window");
959        if (window != null) {
960            WindowPreferences.setPreferences(this, window);
961        }
962
963        List<Element> tfes = e.getChildren("ThrottleFrame");
964        if ((tfes != null) && (!tfes.isEmpty())) {
965            for (int i = 0; i < tfes.size(); i++) {
966                ThrottleFrame tf;
967                if (i == 0) {
968                    tf = getCurentThrottleController();
969                } else {
970                    tf = (ThrottleFrame) newThrottleController();
971                }
972                tf.setXml(tfes.get(i));
973                tf.setEditMode(isEditMode);
974            }
975        }
976
977        List<Element> jinsts = e.getChildren("Jynstrument");
978        if ((jinsts != null) && (!jinsts.isEmpty())) {
979            jinsts.forEach((jinst) -> {
980                Jynstrument jyn = ynstrument(FileUtil.getExternalFilename(jinst.getAttributeValue("JynstrumentFolder")));
981                if (jyn != null) {
982                    jyn.setXml(jinst);
983                }
984            });
985        }
986
987        updateGUI();
988    }
989
990    @Override
991    public synchronized void addPropertyChangeListener(java.beans.PropertyChangeListener l) {
992        pcs.addPropertyChangeListener(l);
993    }
994
995    @Override
996    public synchronized void removePropertyChangeListener(java.beans.PropertyChangeListener l) {
997        pcs.removePropertyChangeListener(l);
998    }
999
1000    private void installInputsListenerOnAllComponents(Container c) {
1001        c.setFocusTraversalKeysEnabled(false); // make tab and shift tab available
1002        if (! ( c instanceof JTextField)) {
1003            c.setFocusable(false);
1004        }
1005        for (Component component : c.getComponents()) {
1006            if (component instanceof Container) {
1007                installInputsListenerOnAllComponents( (Container) component);
1008            } else {
1009                if (! ( component instanceof JTextField)) {
1010                    component.setFocusable(false);
1011                }
1012            }
1013        }
1014    }
1015
1016    private void applyPreferences() {
1017        ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1018        // inputs
1019        ComponentInputMap im = new ComponentInputMap(getRootPane());
1020        for (Object k : this.getRootPane().getActionMap().allKeys()) {
1021            KeyStroke[] kss = preferences.getThrottlesKeyboardControls().getKeyStrokes((String)k);
1022            if (kss !=null) {
1023                for (KeyStroke keystroke : kss) {
1024                    if (keystroke != null) {
1025                        im.put(keystroke, k);
1026                    }
1027                }
1028            }
1029        }
1030        getRootPane().setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW,im);
1031        // UI elements
1032        throttleToolBar.setVisible ( preferences.isUsingExThrottle() && preferences.isUsingToolBar() );
1033        if (smallPowerMgmtButton != null) {
1034            smallPowerMgmtButton.setVisible( (!preferences.isUsingExThrottle()) || (!preferences.isUsingToolBar()) );
1035        }
1036        if (! preferences.isUsingExThrottle()) {        
1037            setEditMode(true);            
1038        } 
1039    }
1040
1041    @Override
1042    public void propertyChange(PropertyChangeEvent evt) {
1043        if (ThrottlesPreferences.prefPopertyName.compareTo(evt.getPropertyName()) == 0) {
1044            applyPreferences();
1045        }        
1046    }
1047
1048    private static final Logger log = LoggerFactory.getLogger(ThrottleWindow.class);
1049}