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