001package jmri.jmrit.throttle;
002
003import java.awt.BorderLayout;
004import java.awt.Color;
005import java.awt.Component;
006import java.awt.Container;
007import java.awt.Dimension;
008import java.awt.Graphics;
009import java.awt.Point;
010import java.awt.Rectangle;
011import java.awt.event.ComponentEvent;
012import java.awt.event.ComponentListener;
013import java.awt.event.ContainerEvent;
014import java.awt.event.ContainerListener;
015import java.beans.PropertyVetoException;
016import java.io.File;
017import java.io.FileNotFoundException;
018import java.io.IOException;
019import java.net.URI;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.List;
023
024import javax.swing.*;
025import javax.swing.event.InternalFrameAdapter;
026import javax.swing.event.InternalFrameEvent;
027
028import jmri.DccLocoAddress;
029import jmri.DccThrottle;
030import jmri.InstanceManager;
031import jmri.LocoAddress;
032import jmri.ThrottleManager;
033import jmri.configurexml.LoadXmlConfigAction;
034import jmri.configurexml.StoreXmlConfigAction;
035import jmri.jmrit.XmlFile;
036import jmri.jmrit.jython.Jynstrument;
037import jmri.jmrit.jython.JynstrumentFactory;
038import jmri.jmrit.roster.RosterEntry;
039import jmri.util.FileUtil;
040import jmri.util.iharder.dnd.URIDrop;
041import jmri.util.swing.JmriJOptionPane;
042
043import org.jdom2.Document;
044import org.jdom2.Element;
045import org.jdom2.JDOMException;
046
047/**
048 * Should be named ThrottlePanel but was already existing with that name and
049 * don't want to break dependencies (particularly in Jython code)
050 *
051 * @author Glen Oberhauser
052 * @author Andrew Berridge Copyright 2010
053 */
054public class ThrottleFrame extends JDesktopPane implements ComponentListener, AddressListener {
055
056    private DccThrottle throttle;
057    private final ThrottleManager throttleManager;
058    private final ThrottlesTableModel allThrottlesTableModel = InstanceManager.getDefault(ThrottleFrameManager.class).getThrottlesListPanel().getTableModel();
059
060    private final Integer BACKPANEL_LAYER = Integer.MIN_VALUE;
061    private final Integer PANEL_LAYER_FRAME = 1;
062    private final Integer PANEL_LAYER_PANEL = 2;
063
064    private static final int ADDRESS_PANEL_INDEX = 0;
065    private static final int CONTROL_PANEL_INDEX = 1;
066    private static final int FUNCTION_PANEL_INDEX = 2;
067    private static final int SPEED_DISPLAY_INDEX = 3;
068    private static final int NUM_FRAMES = 4;
069
070    private JInternalFrame[] frameList;
071    private int activeFrame;
072
073    private final ThrottleWindow throttleWindow;
074
075    private ControlPanel controlPanel;
076    private FunctionPanel functionPanel;
077    private AddressPanel addressPanel;
078    private BackgroundPanel backgroundPanel;
079    private FrameListener frameListener;
080    private SpeedPanel speedPanel;
081
082    private String title;
083    private String lastUsedSaveFile = null;
084
085    private boolean isEditMode = true;
086    private boolean willSwitch = false;
087    private boolean isLoadingDefault = false;
088
089    private static final String DEFAULT_THROTTLE_FILENAME = "JMRI_ThrottlePreference.xml";
090
091    public static String getDefaultThrottleFolder() {
092        return FileUtil.getUserFilesPath() + "throttle" + File.separator;
093    }
094
095    public static String getDefaultThrottleFilename() {
096        return getDefaultThrottleFolder() + DEFAULT_THROTTLE_FILENAME;
097    }
098
099    public ThrottleFrame(ThrottleWindow tw) {
100        this(tw, InstanceManager.getDefault(ThrottleManager.class));
101    }
102
103    public ThrottleFrame(ThrottleWindow tw, ThrottleManager tm) {
104        super();
105        throttleWindow = tw;
106        throttleManager = tm;
107        initGUI();
108        applyPreferences();
109        InstanceManager.getDefault(ThrottleFrameManager.class).getThrottlesListPanel().getTableModel().addThrottleFrame(tw,this);
110    }
111
112    public ThrottleWindow getThrottleWindow() {
113        return throttleWindow;
114    }
115
116    public ControlPanel getControlPanel() {
117        return controlPanel;
118    }
119
120    public FunctionPanel getFunctionPanel() {
121        return functionPanel;
122    }
123
124    public AddressPanel getAddressPanel() {
125        return addressPanel;
126    }
127
128    public RosterEntry getRosterEntry() {
129        return addressPanel.getRosterEntry();
130    }
131
132    public void toFront() {
133        if (throttleWindow == null) {
134            return;
135        }
136        throttleWindow.toFront(title);
137    }
138
139    public SpeedPanel getSpeedPanel() {
140        return speedPanel;
141    }
142
143    /**
144     * Sets the location of a throttle frame on the screen according to x and y
145     * coordinates
146     *
147     * @see java.awt.Component#setLocation(int, int)
148     */
149    @Override
150    public void setLocation(int x, int y) {
151        if (throttleWindow == null) {
152            return;
153        }
154        throttleWindow.setLocation(new Point(x, y));
155    }
156
157    public void setTitle(String txt) {
158        title = txt;
159    }
160
161    public String getTitle() {
162        return title;
163    }
164
165    private void saveThrottle(String sfile) {
166        // Save throttle: title / window position
167        // as strongly linked to extended throttles and roster presence, do not save function buttons and background window as they're stored in the roster entry
168        XmlFile xf = new XmlFile() {
169        };   // odd syntax is due to XmlFile being abstract
170        xf.makeBackupFile(sfile);
171        File file = new File(sfile);
172        try {
173            //The file does not exist, create it before writing
174            File parentDir = file.getParentFile();
175            if (!parentDir.exists()) {
176                if (!parentDir.mkdir()) { // make directory and check result
177                    log.error("could not make parent directory");
178                }
179            }
180            if (!file.createNewFile()) { // create file, check success
181                log.error("createNewFile failed");
182            }
183        } catch (IOException exp) {
184            log.error("Exception while writing the throttle file, may not be complete: {}", exp.getMessage());
185        }
186
187        try {
188            Element root = new Element("throttle-config");
189            root.setAttribute("noNamespaceSchemaLocation",  // NOI18N
190                    "http://jmri.org/xml/schema/throttle-config.xsd",  // NOI18N
191                    org.jdom2.Namespace.getNamespace("xsi",
192                            "http://www.w3.org/2001/XMLSchema-instance"));  // NOI18N
193            Document doc = new Document(root);
194
195            // add XSLT processing instruction
196            // <?xml-stylesheet type="text/xsl" href="XSLT/throttle.xsl"?>
197            java.util.Map<String,String> m = new java.util.HashMap<String, String>();
198            m.put("type", "text/xsl");
199            m.put("href", jmri.jmrit.XmlFile.xsltLocation + "throttle-config.xsl");
200            org.jdom2.ProcessingInstruction p = new org.jdom2.ProcessingInstruction("xml-stylesheet", m);
201            doc.addContent(0,p);
202
203            Element throttleElement = getXml();
204            // don't save the loco address or consist address
205            //   throttleElement.getChild("AddressPanel").removeChild("locoaddress");
206            //   throttleElement.getChild("AddressPanel").removeChild("locoaddress");
207            if ((this.getRosterEntry() != null) &&
208                    (getDefaultThrottleFolder() + addressPanel.getRosterEntry().getId().trim() + ".xml").compareTo(sfile) == 0) // don't save function buttons labels, they're in roster entry
209            {
210                throttleElement.getChild("FunctionPanel").removeChildren("FunctionButton");
211            }
212
213            root.setContent(throttleElement);
214            xf.writeXML(file, doc);
215            setLastUsedSaveFile(sfile);
216        } catch (IOException ex) {
217            log.warn("Exception while storing throttle xml: {}", ex.getMessage());
218        }
219    }
220
221    private void loadDefaultThrottle() {
222        if (isLoadingDefault) { // avoid looping on this method
223            return; 
224        }
225        isLoadingDefault = true;
226        String dtf = InstanceManager.getDefault(ThrottlesPreferences.class).getDefaultThrottleFilePath();
227        if (dtf == null || dtf.isEmpty()) {
228            return;
229        }
230        log.debug("Loading default throttle file : {}", dtf);
231        loadThrottle(dtf);
232        setLastUsedSaveFile(null);
233        isLoadingDefault = false;
234    }
235
236    public void loadThrottle() {
237        JFileChooser fileChooser = jmri.jmrit.XmlFile.userFileChooser(Bundle.getMessage("PromptXmlFileTypes"), "xml");
238        fileChooser.setCurrentDirectory(new File(getDefaultThrottleFolder()));
239        fileChooser.setDialogType(JFileChooser.OPEN_DIALOG);
240        java.io.File file = LoadXmlConfigAction.getFile(fileChooser);
241        if (file == null) {
242            return ;
243        }
244        loadThrottle(file.getAbsolutePath());
245    }
246
247    public void loadThrottle(String sfile) {
248        if (sfile == null) {
249            loadThrottle();
250            return;
251        }
252        log.debug("Loading throttle file : {}", sfile);
253        boolean switchAfter = false;
254        if (!isEditMode) {
255            setEditMode(true);
256            switchAfter = true;
257        }
258
259        try {
260            XmlFile xf = new XmlFile() {
261            };   // odd syntax is due to XmlFile being abstract
262            xf.setValidate(XmlFile.Validate.CheckDtdThenSchema);
263            File f = new File(sfile);
264            Element root = xf.rootFromFile(f);
265            Element conf = root.getChild("ThrottleFrame");
266            // File looks ok
267            setLastUsedSaveFile(sfile);
268            // close all existing Jynstruments
269            Component[] cmps = getComponents();
270            for (Component cmp : cmps) {
271                try {
272                    if (cmp instanceof JInternalFrame) {
273                        JInternalFrame jyf = (JInternalFrame) cmp;
274                        Component[] cmps2 = jyf.getContentPane().getComponents();
275                        for (Component cmp2 : cmps2) {
276                            if (cmp2 instanceof Jynstrument) {
277                                ((Jynstrument) cmp2).exit();
278                                jyf.dispose();
279                            }
280                        }
281                    }
282                } catch (Exception ex) {
283                    log.debug("Got exception (no panic) {}", ex.getMessage());
284                }
285            }
286            // and finally load all preferences
287            setXml(conf);
288        } catch (FileNotFoundException ex) {
289            // Don't show error dialog if file is not found
290            log.debug("Loading throttle exception: {}", ex.getMessage());
291            log.debug("Tried loading throttle file \"{}\" , reverting to default, if any", sfile);
292            loadDefaultThrottle(); // revert to loading default one
293        } catch (NullPointerException | IOException | JDOMException ex) {
294            log.debug("Loading throttle exception: {}", ex.getMessage());
295            log.debug("Tried loading throttle file \"{}\" , reverting to default, if any", sfile);
296            jmri.configurexml.ConfigXmlManager.creationErrorEncountered(
297                    null, "parsing file " + sfile,
298                    "Parse error", null, null, ex);
299            loadDefaultThrottle(); // revert to loading default one
300        }
301//     checkPosition();
302        if (switchAfter) {
303            setEditMode(false);
304        }
305    }
306
307    /**
308     * Place and initialize the GUI elements.
309     * <ul>
310     * <li> ControlPanel
311     * <li> FunctionPanel
312     * <li> AddressPanel
313     * <li> SpeedPanel
314     * <li> JMenu
315     * </ul>
316     */
317    private void initGUI() {
318        frameListener = new FrameListener();
319
320        controlPanel = new ControlPanel(throttleManager);
321        controlPanel.setResizable(true);
322        controlPanel.setClosable(true);
323        controlPanel.setIconifiable(true);
324        controlPanel.setTitle(Bundle.getMessage("ThrottleMenuViewControlPanel"));
325        controlPanel.pack();
326        controlPanel.setVisible(true);
327        controlPanel.setEnabled(false);
328        controlPanel.addInternalFrameListener(frameListener);
329
330        functionPanel = new FunctionPanel();
331        functionPanel.setResizable(true);
332        functionPanel.setClosable(true);
333        functionPanel.setIconifiable(true);
334        functionPanel.setTitle(Bundle.getMessage("ThrottleMenuViewFunctionPanel"));
335
336        // assumes button width of 54, height of 30 (set in class FunctionButton) with
337        // horiz and vert gaps of 5 each (set in FunctionPanel class)
338        // with 3 buttons across and 6 rows high
339        int width = 3 * (FunctionButton.getButtonWidth()) + 2 * 3 * 5 + 10;   // = 192
340        int height = 8 * (FunctionButton.getButtonHeight()) + 2 * 6 * 5 + 20; // = 240 (but there seems to be another 10 needed for some LAFs)
341
342        functionPanel.setSize(width, height);
343        functionPanel.setLocation(controlPanel.getWidth(), 0);
344        functionPanel.setVisible(true);
345        functionPanel.setEnabled(false);
346        functionPanel.addInternalFrameListener(frameListener);
347
348        speedPanel = new SpeedPanel();
349        speedPanel.setResizable(true);
350        speedPanel.setVisible(false);
351        speedPanel.setClosable(true);
352        speedPanel.setIconifiable(true);
353        speedPanel.setTitle(Bundle.getMessage("ThrottleMenuViewSpeedPanel"));
354        speedPanel.addInternalFrameListener(frameListener);
355        speedPanel.pack();
356
357        addressPanel = new AddressPanel(throttleManager);
358        addressPanel.setResizable(true);
359        addressPanel.setClosable(true);
360        addressPanel.setIconifiable(true);
361        addressPanel.setTitle(Bundle.getMessage("ThrottleMenuViewAddressPanel"));
362        addressPanel.pack();
363        if (addressPanel.getWidth()<functionPanel.getWidth()) {
364            addressPanel.setSize(functionPanel.getWidth(),addressPanel.getHeight());
365        }
366        addressPanel.setLocation(controlPanel.getWidth(), functionPanel.getHeight());
367        addressPanel.setVisible(true);
368        addressPanel.addInternalFrameListener(frameListener);
369        functionPanel.setAddressPanel(addressPanel); // so the function panel can get access to the roster
370        controlPanel.setAddressPanel(addressPanel);
371        speedPanel.setAddressPanel(addressPanel);
372
373        if (controlPanel.getHeight() < functionPanel.getHeight() + addressPanel.getHeight()) {
374            controlPanel.setSize(controlPanel.getWidth(), functionPanel.getHeight() + addressPanel.getHeight());
375        }
376        if (controlPanel.getHeight() > functionPanel.getHeight() + addressPanel.getHeight()) {
377            addressPanel.setSize(addressPanel.getWidth(), controlPanel.getHeight() - functionPanel.getHeight());
378        }
379        if (functionPanel.getWidth() < addressPanel.getWidth()) {
380            functionPanel.setSize(addressPanel.getWidth(), functionPanel.getHeight());
381        }
382
383        speedPanel.setSize(addressPanel.getWidth() + controlPanel.getWidth(), addressPanel.getHeight() / 2);
384        speedPanel.setLocation(0, controlPanel.getHeight());
385
386        addressPanel.addAddressListener(controlPanel);
387        addressPanel.addAddressListener(functionPanel);
388        addressPanel.addAddressListener(speedPanel);
389        addressPanel.addAddressListener(this);
390
391        add(controlPanel, PANEL_LAYER_FRAME);
392        add(functionPanel, PANEL_LAYER_FRAME);
393        add(addressPanel, PANEL_LAYER_FRAME);
394        add(speedPanel, PANEL_LAYER_FRAME);
395
396        backgroundPanel = new BackgroundPanel();
397        backgroundPanel.setAddressPanel(addressPanel); // reusing same way to do it than existing thing in functionPanel
398        addComponentListener(backgroundPanel); // backgroudPanel warned when desktop resized
399        addressPanel.addAddressListener(backgroundPanel);
400        addressPanel.setBackgroundPanel(backgroundPanel); // so that it's changeable when browsing through rosters
401        add(backgroundPanel, BACKPANEL_LAYER);
402
403        addComponentListener(this); // to force sub windows repositionning
404
405        frameList = new JInternalFrame[NUM_FRAMES];
406        frameList[ADDRESS_PANEL_INDEX] = addressPanel;
407        frameList[CONTROL_PANEL_INDEX] = controlPanel;
408        frameList[FUNCTION_PANEL_INDEX] = functionPanel;
409        frameList[SPEED_DISPLAY_INDEX] = speedPanel;
410        activeFrame = ADDRESS_PANEL_INDEX;
411
412        setPreferredSize(new Dimension(Math.max(controlPanel.getWidth() + functionPanel.getWidth(), controlPanel.getWidth() + addressPanel.getWidth()),
413                Math.max(addressPanel.getHeight() + functionPanel.getHeight(), controlPanel.getHeight())));
414
415        // #JYNSTRUMENT# Bellow prepare drag'n drop receptacle:
416        new URIDrop(backgroundPanel, uris -> {
417                if (isEditMode) {
418                    for (URI uri : uris ) {
419                        ynstrument(new File(uri).getPath());
420                    }
421                }
422            });
423
424        try {
425            addressPanel.setSelected(true);
426        } catch (PropertyVetoException ex) {
427            log.error("Error selecting InternalFrame: {}", ex.getMessage());
428        }
429    }
430
431    // #JYNSTRUMENT# here instantiate the Jynstrument, put it in a component, initialize the context and start it
432    public JInternalFrame ynstrument(String path) {
433        if (path == null) {
434            return null;
435        }
436        Jynstrument it = JynstrumentFactory.createInstrument(path, this); // everything is there
437        if (it == null) {
438            log.error("Error while creating Jynstrument {}", path);
439            return null;
440        }
441        setTransparentBackground(it);
442        JInternalFrame newiFrame = new JInternalFrame(it.getClassName());
443        newiFrame.add(it);
444        newiFrame.addInternalFrameListener(frameListener);
445        newiFrame.setDoubleBuffered(true);
446        newiFrame.setResizable(true);
447        newiFrame.setClosable(true);
448        newiFrame.setIconifiable(true);
449        newiFrame.getContentPane().addContainerListener(new ContainerListener() {
450            @Override
451            public void componentAdded(ContainerEvent e) {
452            }
453
454            @Override
455            public void componentRemoved(ContainerEvent e) {
456                Container c = e.getContainer();
457                while ((!(c instanceof JInternalFrame)) && (!(c instanceof TranslucentJPanel))) {
458                    c = c.getParent();
459                }
460                c.setVisible(false);
461                remove(c);
462                repaint();
463            }
464        });
465        newiFrame.pack();
466        add(newiFrame, PANEL_LAYER_FRAME);
467        newiFrame.setVisible(true);
468        return newiFrame;
469    }
470
471    // make sure components are inside this frame bounds
472    private void checkPosition(Component comp) {
473        if ((this.getWidth() < 1) || (this.getHeight() < 1)) {
474            return;
475        }
476
477        Rectangle pos = comp.getBounds();
478
479        if (pos.width > this.getWidth()) { // Component largest than container
480            pos.width = this.getWidth() - 2;
481            pos.x = 1;
482        }
483        if (pos.x + pos.width > this.getWidth()) // Component to large
484        {
485            pos.x = this.getWidth() - pos.width - 1;
486        }
487        if (pos.x < 0) // Component to far on the left
488        {
489            pos.x = 1;
490        }
491
492        if (pos.height > this.getHeight()) { // Component higher than container
493            pos.height = this.getHeight() - 2;
494            pos.y = 1;
495        }
496        if (pos.y + pos.height > this.getHeight()) // Component to low
497        {
498            pos.y = this.getHeight() - pos.height - 1;
499        }
500        if (pos.y < 0) // Component to high
501        {
502            pos.y = 1;
503        }
504
505        comp.setBounds(pos);
506    }
507
508    public void makeAllComponentsInBounds() {
509        Component[] cmps = getComponents();
510        for (Component cmp : cmps) {
511            checkPosition(cmp);
512        }
513    }
514
515    private HashMap<Container, JInternalFrame> contentPanes;
516
517    public void applyPreferences() {
518        ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
519
520        backgroundPanel.setVisible(  (preferences.isUsingExThrottle()) && (preferences.isUsingRosterImage()));
521
522        controlPanel.applyPreferences();
523        functionPanel.applyPreferences();
524        addressPanel.applyPreferences();
525        backgroundPanel.applyPreferences();
526    }
527
528    private static class TranslucentJPanel extends JPanel {
529
530        private final Color TRANS_COL = new Color(100, 100, 100, 100);
531
532        public TranslucentJPanel() {
533            super();
534            setOpaque(false);
535        }
536
537        @Override
538        public void paintComponent(Graphics g) {
539            g.setColor(TRANS_COL);
540            g.fillRoundRect(0, 0, getSize().width, getSize().height, 10, 10);
541            super.paintComponent(g);
542        }
543    }
544
545    private void playRendering() {
546        Component[] cmps = getComponentsInLayer(PANEL_LAYER_FRAME);
547        contentPanes = new HashMap<>();
548        for (Component cmp : cmps) {
549            if ((cmp instanceof JInternalFrame) && (cmp.isVisible())) {
550                translude((JInternalFrame)cmp);
551            }
552        }
553    }
554
555    private void translude(JInternalFrame jif) {
556        Dimension cpSize = jif.getContentPane().getSize();
557        Point cpLoc = jif.getContentPane().getLocationOnScreen();
558        TranslucentJPanel pane = new TranslucentJPanel();
559        pane.setLayout(new BorderLayout());
560        contentPanes.put(pane, jif);
561        pane.add(jif.getContentPane(), BorderLayout.CENTER);
562        setTransparent(pane, true);
563        jif.setContentPane(new JPanel());
564        jif.setVisible(false);
565        Point loc = new Point(cpLoc.x - this.getLocationOnScreen().x, cpLoc.y - this.getLocationOnScreen().y);
566        add(pane, PANEL_LAYER_PANEL);
567        pane.setLocation(loc);
568        pane.setSize(cpSize);
569    }
570
571    private void editRendering() {
572        Component[] cmps = getComponentsInLayer(PANEL_LAYER_PANEL);
573        for (Component cmp : cmps) {
574            if (cmp instanceof JPanel) {
575                JPanel pane = (JPanel) cmp;
576                JInternalFrame jif = contentPanes.get(pane);
577                jif.setContentPane((Container) pane.getComponent(0));
578                setTransparent(jif, false);
579                jif.setVisible(true);
580                remove(pane);
581            }
582        }
583    }
584
585    public void setEditMode(boolean mode) {
586        if (mode == isEditMode)
587            return;
588        if (isVisible()) {
589            if (!mode) {
590                playRendering();
591            } else {
592                editRendering();
593            }
594            isEditMode = mode;
595            willSwitch = false;
596        } else {
597            willSwitch = true;
598        }
599        throttleWindow.updateGUI();
600    }
601
602    public boolean getEditMode() {
603        return isEditMode;
604    }
605
606    /**
607     * Handle my own destruction.
608     * <ol>
609     * <li> dispose of sub windows.
610     * <li> notify my manager of my demise.
611     * </ol>
612     */
613    public void dispose() {
614        log.debug("Disposing {}", getTitle());
615        URIDrop.remove(backgroundPanel);
616        addressPanel.removeAddressListener(this);
617        // should the throttle list table stop listening to that throttle?
618        if (throttle!=null &&  allThrottlesTableModel.getNumberOfEntriesFor((DccLocoAddress) throttle.getLocoAddress()) == 1 ) {
619            throttleManager.removeListener(throttle.getLocoAddress(), allThrottlesTableModel);
620            allThrottlesTableModel.fireTableDataChanged();
621        }
622        
623        // remove from the throttle list table
624        InstanceManager.getDefault(ThrottleFrameManager.class).getThrottlesListPanel().getTableModel().removeThrottleFrame(this, addressPanel.getCurrentAddress());
625        // check for any special disposing in InternalFrames
626        controlPanel.destroy();
627        functionPanel.destroy();
628        speedPanel.destroy();
629        backgroundPanel.destroy();
630        // dispose of this last because it will release and destroy the throttle.
631        addressPanel.destroy();
632    }
633
634    public void saveRosterChanges() {
635        RosterEntry rosterEntry = addressPanel.getRosterEntry();
636        if (rosterEntry == null) {
637            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ThrottleFrameNoRosterItemMessageDialog"),
638                Bundle.getMessage("ThrottleFrameNoRosterItemTitleDialog"), JmriJOptionPane.ERROR_MESSAGE);
639            return;
640        }
641        if (JmriJOptionPane.showConfirmDialog(this, Bundle.getMessage("ThrottleFrameRosterChangeMesageDialog"),
642            Bundle.getMessage("ThrottleFrameRosterChangeTitleDialog"), JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION) {
643            return;
644        }
645        functionPanel.saveFunctionButtonsToRoster(rosterEntry);
646        controlPanel.saveToRoster(rosterEntry);
647    }
648
649    /**
650     * An extension of InternalFrameAdapter for listening to the closing of of
651     * this frame's internal frames.
652     *
653     * @author glen
654     */
655    class FrameListener extends InternalFrameAdapter {
656
657        /**
658         * Listen for the closing of an internal frame and set the "View" menu
659         * appropriately. Then hide the closing frame
660         *
661         * @param e The InternalFrameEvent leading to this action
662         */
663        @Override
664        public void internalFrameClosing(InternalFrameEvent e) {
665            if (e.getSource() == controlPanel) {
666                throttleWindow.getViewControlPanel().setSelected(false);
667                controlPanel.setVisible(false);
668            } else if (e.getSource() == addressPanel) {
669                throttleWindow.getViewAddressPanel().setSelected(false);
670                addressPanel.setVisible(false);
671            } else if (e.getSource() == functionPanel) {
672                throttleWindow.getViewFunctionPanel().setSelected(false);
673                functionPanel.setVisible(false);
674            } else if (e.getSource() == speedPanel) {
675                throttleWindow.getViewSpeedPanel().setSelected(false);
676                speedPanel.setVisible(false);
677            } else {
678                try { // #JYNSTRUMENT#, Very important, clean the Jynstrument
679                    if ((e.getSource() instanceof JInternalFrame)) {
680                        Component[] cmps = ((JInternalFrame) e.getSource()).getContentPane().getComponents();
681                        int i = 0;
682                        while ((i < cmps.length) && (!(cmps[i] instanceof Jynstrument))) {
683                            i++;
684                        }
685                        if ((i < cmps.length) && (cmps[i] instanceof Jynstrument)) {
686                            ((Jynstrument) cmps[i]).exit();
687                        }
688                    }
689                } catch (Exception exc) {
690                    log.debug("Got exception, can ignore: ", exc);
691                }
692            }
693        }
694
695        /**
696         * Listen for the activation of an internal frame record this property
697         * for correct processing of the frame cycling key.
698         *
699         * @param e The InternalFrameEvent leading to this action
700         */
701        @Override
702        public void internalFrameActivated(InternalFrameEvent e) {
703            if (e.getSource() == controlPanel) {
704                activeFrame = CONTROL_PANEL_INDEX;
705            } else if (e.getSource() == addressPanel) {
706                activeFrame = ADDRESS_PANEL_INDEX;
707            } else if (e.getSource() == functionPanel) {
708                activeFrame = FUNCTION_PANEL_INDEX;
709            } else if (e.getSource() == functionPanel) {
710                activeFrame = SPEED_DISPLAY_INDEX;
711            }
712        }
713    }
714
715    /**
716     * Collect the prefs of this object into XML Element
717     * <ul>
718     * <li> Window prefs
719     * <li> ControlPanel
720     * <li> FunctionPanel
721     * <li> AddressPanel
722     * <li> SpeedPanel
723     * </ul>
724     *
725     *
726     * @return the XML of this object.
727     */
728    public Element getXml() {
729        boolean switchAfter = false;
730        if (!isEditMode) {
731            setEditMode(true);
732            switchAfter = true;
733        }
734
735        Element me = new Element("ThrottleFrame");
736
737        if (((javax.swing.plaf.basic.BasicInternalFrameUI) getControlPanel().getUI()).getNorthPane() != null) {
738            Dimension bDim = ((javax.swing.plaf.basic.BasicInternalFrameUI) getControlPanel().getUI()).getNorthPane().getPreferredSize();
739            me.setAttribute("border", Integer.toString(bDim.height));
740        }
741
742        ArrayList<Element> children = new ArrayList<>(1);
743
744//        children.add(WindowPreferences.getPreferences(this));  // not required as it is in ThrottleWindow
745        children.add(controlPanel.getXml());
746        children.add(functionPanel.getXml());
747        children.add(addressPanel.getXml());
748        children.add(speedPanel.getXml());
749        // Save Jynstruments
750        Component[] cmps = getComponents();
751        for (Component cmp : cmps) {
752            try {
753                if (cmp instanceof JInternalFrame) {
754                    Component[] cmps2 = ((JInternalFrame) cmp).getContentPane().getComponents();
755                    int j = 0;
756                    while ((j < cmps2.length) && (!(cmps2[j] instanceof Jynstrument))) {
757                        j++;
758                    }
759                    if ((j < cmps2.length) && (cmps2[j] instanceof Jynstrument)) {
760                        Jynstrument jyn = (Jynstrument) cmps2[j];
761                        Element elt = new Element("Jynstrument");
762                        elt.setAttribute("JynstrumentFolder", FileUtil.getPortableFilename(jyn.getFolder()));
763                        ArrayList<Element> jychildren = new ArrayList<>(1);
764                        jychildren.add(WindowPreferences.getPreferences((JInternalFrame) cmp));
765                        Element je = jyn.getXml();
766                        if (je != null) {
767                            jychildren.add(je);
768                        }
769                        elt.setContent(jychildren);
770                        children.add(elt);
771                    }
772                }
773            } catch (Exception ex) {
774                log.debug("Got exception (no panic) {}", ex.getMessage());
775            }
776        }
777        me.setContent(children);
778        if (switchAfter) {
779            setEditMode(false);
780        }
781        return me;
782    }
783
784    public Element getXmlFile() {
785        if (getLastUsedSaveFile() == null) { // || (getRosterEntry()==null))
786            return null;
787        }
788        Element me = new Element("ThrottleFrame");
789        me.setAttribute("ThrottleXMLFile", FileUtil.getPortableFilename(getLastUsedSaveFile()));
790        return me;
791    }
792
793    /**
794     * Set the preferences based on the XML Element.
795     * <ul>
796     * <li> Window prefs
797     * <li> Frame title
798     * <li> ControlPanel
799     * <li> FunctionPanel
800     * <li> AddressPanel
801     * <li> SpeedPanel
802     * </ul>
803     *
804     * @param e The Element for this object.
805     */
806    public void setXml(Element e) {
807        if (e == null) {
808            return;
809        }
810
811        String sfile = e.getAttributeValue("ThrottleXMLFile");
812        if (sfile != null) {
813            loadThrottle(FileUtil.getExternalFilename(sfile));
814            return;
815        }
816
817        boolean switchAfter = false;
818        if (!isEditMode) {
819            setEditMode(true);
820            switchAfter = true;
821        }
822
823        int bSize = 23;
824        // Get InternalFrame border size
825        if (e.getAttribute("border") != null) {
826            bSize = Integer.parseInt((e.getAttribute("border").getValue()));
827        }
828        Element controlPanelElement = e.getChild("ControlPanel");
829        controlPanel.setXml(controlPanelElement);
830        if (((javax.swing.plaf.basic.BasicInternalFrameUI) controlPanel.getUI()).getNorthPane() != null) {
831            ((javax.swing.plaf.basic.BasicInternalFrameUI) controlPanel.getUI()).getNorthPane().setPreferredSize(new Dimension(0, bSize));
832        }
833        Element functionPanelElement = e.getChild("FunctionPanel");
834        functionPanel.setXml(functionPanelElement);
835        if (((javax.swing.plaf.basic.BasicInternalFrameUI) functionPanel.getUI()).getNorthPane() != null) {
836            ((javax.swing.plaf.basic.BasicInternalFrameUI) functionPanel.getUI()).getNorthPane().setPreferredSize(new Dimension(0, bSize));
837        }
838        Element addressPanelElement = e.getChild("AddressPanel");
839        addressPanel.setXml(addressPanelElement);
840        if (((javax.swing.plaf.basic.BasicInternalFrameUI) addressPanel.getUI()).getNorthPane() != null) {
841            ((javax.swing.plaf.basic.BasicInternalFrameUI) addressPanel.getUI()).getNorthPane().setPreferredSize(new Dimension(0, bSize));
842        }
843        Element speedPanelElement = e.getChild("SpeedPanel");
844        if (speedPanelElement != null) { // older throttle configs may not have this element
845            speedPanel.setXml(speedPanelElement);
846            if (((javax.swing.plaf.basic.BasicInternalFrameUI) speedPanel.getUI()).getNorthPane() != null) {
847                ((javax.swing.plaf.basic.BasicInternalFrameUI) speedPanel.getUI()).getNorthPane().setPreferredSize(new Dimension(0, bSize));
848            }
849        }
850
851        List<Element> jinsts = e.getChildren("Jynstrument");
852        if ((jinsts != null) && (jinsts.size() > 0)) {
853            for (Element jinst : jinsts) {
854                JInternalFrame jif = ynstrument(FileUtil.getExternalFilename(jinst.getAttributeValue("JynstrumentFolder")));
855                Element window = jinst.getChild("window");
856                if (jif != null) {
857                    if (window != null) {
858                        WindowPreferences.setPreferences(jif, window);
859                    }
860                    Component[] cmps2 = jif.getContentPane().getComponents();
861                    int j = 0;
862                    while ((j < cmps2.length) && (!(cmps2[j] instanceof Jynstrument))) {
863                        j++;
864                    }
865                    if ((j < cmps2.length) && (cmps2[j] instanceof Jynstrument)) {
866                        ((Jynstrument) cmps2[j]).setXml(jinst);
867                    }
868
869                    jif.repaint();
870                }
871            }
872        }
873        setFrameTitle();
874        if (switchAfter) {
875            setEditMode(false);
876        }
877    }
878
879    /**
880     * setFrameTitle - set the frame title based on type, text and address
881     */
882    public void setFrameTitle() {
883        String addr = Bundle.getMessage("ThrottleTitle");
884        if (addressPanel.getThrottle() != null) {
885            addr = addressPanel.getCurrentAddress().toString();
886        }
887        if (throttleWindow.getTitleTextType().compareTo("address") == 0) {
888            throttleWindow.setTitle(addr);
889        } else if (throttleWindow.getTitleTextType().compareTo("text") == 0) {
890            throttleWindow.setTitle(throttleWindow.getTitleText());
891        } else if (throttleWindow.getTitleTextType().compareTo("addressText") == 0) {
892            throttleWindow.setTitle(addr + " " + throttleWindow.getTitleText());
893        } else if (throttleWindow.getTitleTextType().compareTo("textAddress") == 0) {
894            throttleWindow.setTitle(throttleWindow.getTitleText() + " " + addr);
895        } else if (throttleWindow.getTitleTextType().compareTo("rosterID") == 0) {
896            if ((addressPanel.getRosterEntry() != null) && (addressPanel.getRosterEntry().getId() != null)
897                    && (addressPanel.getRosterEntry().getId().length() > 0)) {
898                throttleWindow.setTitle(addressPanel.getRosterEntry().getId());
899            } else {
900                throttleWindow.setTitle(addr);
901            }
902        }
903    }
904
905    @Override
906    public void componentHidden(ComponentEvent e) {
907    }
908
909    @Override
910    public void componentMoved(ComponentEvent e) {
911    }
912
913    @Override
914    public void componentResized(ComponentEvent e) {
915//  checkPosition ();
916    }
917
918    @Override
919    public void componentShown(ComponentEvent e) {
920        throttleWindow.setCurrentThrottleFrame(this);
921        if (willSwitch) {
922            setEditMode(this.throttleWindow.isEditMode());
923            repaint();
924        }
925        throttleWindow.updateGUI();
926        // bring addresspanel to front if no allocated throttle
927        if (addressPanel.getThrottle() == null && throttleWindow.isEditMode()) {
928            if (!addressPanel.isVisible()) {
929                addressPanel.setVisible(true);
930            }
931            if (addressPanel.isIcon()) {
932                try {
933                    addressPanel.setIcon(false);
934                } catch (PropertyVetoException ex) {
935                    log.debug("JInternalFrame uniconify, vetoed");
936                }
937            }
938            addressPanel.requestFocus();
939            addressPanel.toFront();
940            try {
941                addressPanel.setSelected(true);
942            } catch (java.beans.PropertyVetoException ex) {
943                log.debug("JInternalFrame selection, vetoed");
944            }
945        }
946    }
947
948    public void saveThrottle() {
949        if (getRosterEntry() != null) {
950            saveThrottle(getDefaultThrottleFolder() + addressPanel.getRosterEntry().getId().trim() + ".xml");
951        } else if (getLastUsedSaveFile() != null) {
952            saveThrottle(getLastUsedSaveFile());
953        }
954    }
955
956    public void saveThrottleAs() {
957        JFileChooser fileChooser = jmri.jmrit.XmlFile.userFileChooser(Bundle.getMessage("PromptXmlFileTypes"), "xml");
958        fileChooser.setCurrentDirectory(new File(getDefaultThrottleFolder()));
959        fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
960        java.io.File file = StoreXmlConfigAction.getFileName(fileChooser);
961        if (file == null) {
962            return;
963        }
964        saveThrottle(file.getAbsolutePath());
965    }
966
967    public void activateNextJInternalFrame() {
968        try {
969            int initialFrame = activeFrame; // avoid infinite loop
970            do {
971                activeFrame = (activeFrame + 1) % NUM_FRAMES;
972                frameList[activeFrame].setSelected(true);
973            } while ((frameList[activeFrame].isClosed() || frameList[activeFrame].isIcon() || (!frameList[activeFrame].isVisible())) && (initialFrame != activeFrame));
974        } catch (PropertyVetoException ex) {
975            log.warn("Exception selecting internal frame:{}", ex.getMessage());
976        }
977    }
978
979    public void activatePreviousJInternalFrame() {
980        try {
981            int initialFrame = activeFrame; // avoid infinite loop
982            do {
983                activeFrame--;
984                if (activeFrame < 0) {
985                    activeFrame = NUM_FRAMES - 1;
986                }
987                frameList[activeFrame].setSelected(true);
988            } while ((frameList[activeFrame].isClosed() || frameList[activeFrame].isIcon() || (!frameList[activeFrame].isVisible())) && (initialFrame != activeFrame));
989        } catch (PropertyVetoException ex) {
990            log.warn("Exception selecting internal frame:{}", ex.getMessage());
991        }
992    }
993
994    @Override
995    public void notifyAddressChosen(LocoAddress l) {
996    }
997
998    @Override
999    public void notifyAddressReleased(LocoAddress la) {
1000        if (throttle == null) {
1001            log.debug("notifyAddressReleased() throttle already null, called for loc {}",la);
1002            return;
1003        }
1004        setLastUsedSaveFile(null);
1005        setFrameTitle();
1006        throttleWindow.updateGUI(); 
1007        if (throttle!=null && allThrottlesTableModel.getNumberOfEntriesFor((DccLocoAddress) throttle.getLocoAddress()) == 1 )  {
1008            throttleManager.removeListener(throttle.getLocoAddress(), allThrottlesTableModel);
1009        }
1010        allThrottlesTableModel.fireTableDataChanged();
1011        throttle = null;
1012    }
1013
1014    @Override
1015    public void notifyAddressThrottleFound(DccThrottle t) {
1016        if (throttle != null) {
1017            log.debug("notifyAddressThrottleFound() throttle non null, called for loc {}",t.getLocoAddress());
1018            return;
1019        }
1020        throttle = t;
1021        if ((InstanceManager.getDefault(ThrottlesPreferences.class).isUsingExThrottle())
1022                && (InstanceManager.getDefault(ThrottlesPreferences.class).isAutoLoading()) && (addressPanel != null)) {
1023            if ((addressPanel.getRosterEntry() != null)
1024                    && ((getLastUsedSaveFile() == null) || (getLastUsedSaveFile().compareTo(getDefaultThrottleFolder() + addressPanel.getRosterEntry().getId().trim() + ".xml") != 0))) {
1025                loadThrottle(getDefaultThrottleFolder() + addressPanel.getRosterEntry().getId().trim() + ".xml");
1026                setLastUsedSaveFile(getDefaultThrottleFolder() + addressPanel.getRosterEntry().getId().trim() + ".xml");
1027            } else if ((addressPanel.getRosterEntry() == null)
1028                    && ((getLastUsedSaveFile() == null) || (getLastUsedSaveFile().compareTo(getDefaultThrottleFolder() + addressPanel.getCurrentAddress()+ ".xml") != 0))) {
1029                loadThrottle(getDefaultThrottleFolder() + throttle.getLocoAddress().getNumber() + ".xml");
1030                setLastUsedSaveFile(getDefaultThrottleFolder() + throttle.getLocoAddress().getNumber() + ".xml");
1031            }
1032        } else {
1033            if ((addressPanel != null) && (addressPanel.getRosterEntry() == null)) { // no known roster entry
1034                loadDefaultThrottle();
1035            }
1036        }
1037        setFrameTitle();
1038        throttleWindow.updateGUI();
1039        throttleManager.attachListener(throttle.getLocoAddress(), allThrottlesTableModel);        
1040        allThrottlesTableModel.fireTableDataChanged();
1041    }
1042
1043    
1044    @Override
1045    public void notifyConsistAddressChosen(LocoAddress l) {
1046        notifyAddressChosen(l);
1047    }
1048
1049    
1050    @Override
1051    public void notifyConsistAddressReleased(LocoAddress la) {
1052        notifyAddressReleased(la);
1053    }
1054
1055    @Override
1056    public void notifyConsistAddressThrottleFound(DccThrottle throttle) {
1057        notifyAddressThrottleFound(throttle);
1058    }
1059
1060    public String getLastUsedSaveFile() {
1061        return lastUsedSaveFile;
1062    }
1063
1064    public void setLastUsedSaveFile(String lusf) {
1065        lastUsedSaveFile = lusf;
1066        throttleWindow.updateGUI();
1067    }
1068
1069    // some utilities to turn a component background transparent
1070    public static void setTransparentBackground(JComponent jcomp) {
1071        if (jcomp instanceof JPanel) //OS X: Jpanel components are enough
1072        {
1073            jcomp.setBackground(new Color(0, 0, 0, 0));
1074        }
1075        setTransparentBackground(jcomp.getComponents());
1076    }
1077
1078    public static void setTransparentBackground(Component[] comps) {
1079        for (Component comp : comps) {
1080            try {
1081                if (comp instanceof JComponent) {
1082                    setTransparentBackground((JComponent) comp);
1083                }
1084            } catch (Exception e) {
1085                // Do nothing, just go on
1086            }
1087        }
1088    }
1089
1090// some utilities to turn a component background transparent
1091    public static void setTransparent(JComponent jcomp) {
1092        setTransparent(jcomp, true);
1093    }
1094
1095    public static void setTransparent(JComponent jcomp, boolean transparency) {
1096        if (jcomp instanceof JPanel) { //OS X: Jpanel components are enough
1097            jcomp.setOpaque(!transparency);
1098        }
1099        setTransparent(jcomp.getComponents(), transparency);
1100    }
1101
1102    private static void setTransparent(Component[] comps, boolean transparency) {
1103        for (Component comp : comps) {
1104            try {
1105                if (comp instanceof JComponent) {
1106                    setTransparent((JComponent) comp, transparency);
1107                }
1108            } catch (Exception e) {
1109                // Do nothing, just go on
1110            }
1111        }
1112    }
1113
1114    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ThrottleFrame.class);
1115}