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        loadDefaultThrottle();
528    }
529
530    private static class TranslucentJPanel extends JPanel {
531
532        private final Color TRANS_COL = new Color(100, 100, 100, 100);
533
534        public TranslucentJPanel() {
535            super();
536            setOpaque(false);
537        }
538
539        @Override
540        public void paintComponent(Graphics g) {
541            g.setColor(TRANS_COL);
542            g.fillRoundRect(0, 0, getSize().width, getSize().height, 10, 10);
543            super.paintComponent(g);
544        }
545    }
546
547    private void playRendering() {
548        Component[] cmps = getComponentsInLayer(PANEL_LAYER_FRAME);
549        contentPanes = new HashMap<>();
550        for (Component cmp : cmps) {
551            if ((cmp instanceof JInternalFrame) && (cmp.isVisible())) {
552                translude((JInternalFrame)cmp);
553            }
554        }
555    }
556
557    private void translude(JInternalFrame jif) {
558        Dimension cpSize = jif.getContentPane().getSize();
559        Point cpLoc = jif.getContentPane().getLocationOnScreen();
560        TranslucentJPanel pane = new TranslucentJPanel();
561        pane.setLayout(new BorderLayout());
562        contentPanes.put(pane, jif);
563        pane.add(jif.getContentPane(), BorderLayout.CENTER);
564        setTransparent(pane, true);
565        jif.setContentPane(new JPanel());
566        jif.setVisible(false);
567        Point loc = new Point(cpLoc.x - this.getLocationOnScreen().x, cpLoc.y - this.getLocationOnScreen().y);
568        add(pane, PANEL_LAYER_PANEL);
569        pane.setLocation(loc);
570        pane.setSize(cpSize);
571    }
572
573    private void editRendering() {
574        Component[] cmps = getComponentsInLayer(PANEL_LAYER_PANEL);
575        for (Component cmp : cmps) {
576            if (cmp instanceof JPanel) {
577                JPanel pane = (JPanel) cmp;
578                JInternalFrame jif = contentPanes.get(pane);
579                jif.setContentPane((Container) pane.getComponent(0));
580                setTransparent(jif, false);
581                jif.setVisible(true);
582                remove(pane);
583            }
584        }
585    }
586
587    public void setEditMode(boolean mode) {
588        if (mode == isEditMode)
589            return;
590        if (isVisible()) {
591            if (!mode) {
592                playRendering();
593            } else {
594                editRendering();
595            }
596            isEditMode = mode;
597            willSwitch = false;
598        } else {
599            willSwitch = true;
600        }
601        throttleWindow.updateGUI();
602    }
603
604    public boolean getEditMode() {
605        return isEditMode;
606    }
607
608    /**
609     * Handle my own destruction.
610     * <ol>
611     * <li> dispose of sub windows.
612     * <li> notify my manager of my demise.
613     * </ol>
614     */
615    public void dispose() {
616        log.debug("Disposing {}", getTitle());
617        URIDrop.remove(backgroundPanel);
618        addressPanel.removeAddressListener(this);
619        // should the throttle list table stop listening to that throttle?
620        if (throttle!=null &&  allThrottlesTableModel.getNumberOfEntriesFor((DccLocoAddress) throttle.getLocoAddress()) == 1 ) {
621            throttleManager.removeListener(throttle.getLocoAddress(), allThrottlesTableModel);
622            allThrottlesTableModel.fireTableDataChanged();
623        }
624        
625        // remove from the throttle list table
626        InstanceManager.getDefault(ThrottleFrameManager.class).getThrottlesListPanel().getTableModel().removeThrottleFrame(this, addressPanel.getCurrentAddress());
627        // check for any special disposing in InternalFrames
628        controlPanel.destroy();
629        functionPanel.destroy();
630        speedPanel.destroy();
631        backgroundPanel.destroy();
632        // dispose of this last because it will release and destroy the throttle.
633        addressPanel.destroy();
634    }
635
636    public void saveRosterChanges() {
637        RosterEntry rosterEntry = addressPanel.getRosterEntry();
638        if (rosterEntry == null) {
639            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ThrottleFrameNoRosterItemMessageDialog"),
640                Bundle.getMessage("ThrottleFrameNoRosterItemTitleDialog"), JmriJOptionPane.ERROR_MESSAGE);
641            return;
642        }
643        if (JmriJOptionPane.showConfirmDialog(this, Bundle.getMessage("ThrottleFrameRosterChangeMesageDialog"),
644            Bundle.getMessage("ThrottleFrameRosterChangeTitleDialog"), JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION) {
645            return;
646        }
647        functionPanel.saveFunctionButtonsToRoster(rosterEntry);
648        controlPanel.saveToRoster(rosterEntry);
649    }
650
651    /**
652     * An extension of InternalFrameAdapter for listening to the closing of of
653     * this frame's internal frames.
654     *
655     * @author glen
656     */
657    class FrameListener extends InternalFrameAdapter {
658
659        /**
660         * Listen for the closing of an internal frame and set the "View" menu
661         * appropriately. Then hide the closing frame
662         *
663         * @param e The InternalFrameEvent leading to this action
664         */
665        @Override
666        public void internalFrameClosing(InternalFrameEvent e) {
667            if (e.getSource() == controlPanel) {
668                throttleWindow.getViewControlPanel().setSelected(false);
669                controlPanel.setVisible(false);
670            } else if (e.getSource() == addressPanel) {
671                throttleWindow.getViewAddressPanel().setSelected(false);
672                addressPanel.setVisible(false);
673            } else if (e.getSource() == functionPanel) {
674                throttleWindow.getViewFunctionPanel().setSelected(false);
675                functionPanel.setVisible(false);
676            } else if (e.getSource() == speedPanel) {
677                throttleWindow.getViewSpeedPanel().setSelected(false);
678                speedPanel.setVisible(false);
679            } else {
680                try { // #JYNSTRUMENT#, Very important, clean the Jynstrument
681                    if ((e.getSource() instanceof JInternalFrame)) {
682                        Component[] cmps = ((JInternalFrame) e.getSource()).getContentPane().getComponents();
683                        int i = 0;
684                        while ((i < cmps.length) && (!(cmps[i] instanceof Jynstrument))) {
685                            i++;
686                        }
687                        if ((i < cmps.length) && (cmps[i] instanceof Jynstrument)) {
688                            ((Jynstrument) cmps[i]).exit();
689                        }
690                    }
691                } catch (Exception exc) {
692                    log.debug("Got exception, can ignore: ", exc);
693                }
694            }
695        }
696
697        /**
698         * Listen for the activation of an internal frame record this property
699         * for correct processing of the frame cycling key.
700         *
701         * @param e The InternalFrameEvent leading to this action
702         */
703        @Override
704        public void internalFrameActivated(InternalFrameEvent e) {
705            if (e.getSource() == controlPanel) {
706                activeFrame = CONTROL_PANEL_INDEX;
707            } else if (e.getSource() == addressPanel) {
708                activeFrame = ADDRESS_PANEL_INDEX;
709            } else if (e.getSource() == functionPanel) {
710                activeFrame = FUNCTION_PANEL_INDEX;
711            } else if (e.getSource() == functionPanel) {
712                activeFrame = SPEED_DISPLAY_INDEX;
713            }
714        }
715    }
716
717    /**
718     * Collect the prefs of this object into XML Element
719     * <ul>
720     * <li> Window prefs
721     * <li> ControlPanel
722     * <li> FunctionPanel
723     * <li> AddressPanel
724     * <li> SpeedPanel
725     * </ul>
726     *
727     *
728     * @return the XML of this object.
729     */
730    public Element getXml() {
731        boolean switchAfter = false;
732        if (!isEditMode) {
733            setEditMode(true);
734            switchAfter = true;
735        }
736
737        Element me = new Element("ThrottleFrame");
738
739        if (((javax.swing.plaf.basic.BasicInternalFrameUI) getControlPanel().getUI()).getNorthPane() != null) {
740            Dimension bDim = ((javax.swing.plaf.basic.BasicInternalFrameUI) getControlPanel().getUI()).getNorthPane().getPreferredSize();
741            me.setAttribute("border", Integer.toString(bDim.height));
742        }
743
744        ArrayList<Element> children = new ArrayList<>(1);
745
746//        children.add(WindowPreferences.getPreferences(this));  // not required as it is in ThrottleWindow
747        children.add(controlPanel.getXml());
748        children.add(functionPanel.getXml());
749        children.add(addressPanel.getXml());
750        children.add(speedPanel.getXml());
751        // Save Jynstruments
752        Component[] cmps = getComponents();
753        for (Component cmp : cmps) {
754            try {
755                if (cmp instanceof JInternalFrame) {
756                    Component[] cmps2 = ((JInternalFrame) cmp).getContentPane().getComponents();
757                    int j = 0;
758                    while ((j < cmps2.length) && (!(cmps2[j] instanceof Jynstrument))) {
759                        j++;
760                    }
761                    if ((j < cmps2.length) && (cmps2[j] instanceof Jynstrument)) {
762                        Jynstrument jyn = (Jynstrument) cmps2[j];
763                        Element elt = new Element("Jynstrument");
764                        elt.setAttribute("JynstrumentFolder", FileUtil.getPortableFilename(jyn.getFolder()));
765                        ArrayList<Element> jychildren = new ArrayList<>(1);
766                        jychildren.add(WindowPreferences.getPreferences((JInternalFrame) cmp));
767                        Element je = jyn.getXml();
768                        if (je != null) {
769                            jychildren.add(je);
770                        }
771                        elt.setContent(jychildren);
772                        children.add(elt);
773                    }
774                }
775            } catch (Exception ex) {
776                log.debug("Got exception (no panic) {}", ex.getMessage());
777            }
778        }
779        me.setContent(children);
780        if (switchAfter) {
781            setEditMode(false);
782        }
783        return me;
784    }
785
786    public Element getXmlFile() {
787        if (getLastUsedSaveFile() == null) { // || (getRosterEntry()==null))
788            return null;
789        }
790        Element me = new Element("ThrottleFrame");
791        me.setAttribute("ThrottleXMLFile", FileUtil.getPortableFilename(getLastUsedSaveFile()));
792        return me;
793    }
794
795    /**
796     * Set the preferences based on the XML Element.
797     * <ul>
798     * <li> Window prefs
799     * <li> Frame title
800     * <li> ControlPanel
801     * <li> FunctionPanel
802     * <li> AddressPanel
803     * <li> SpeedPanel
804     * </ul>
805     *
806     * @param e The Element for this object.
807     */
808    public void setXml(Element e) {
809        if (e == null) {
810            return;
811        }
812
813        String sfile = e.getAttributeValue("ThrottleXMLFile");
814        if (sfile != null) {
815            loadThrottle(FileUtil.getExternalFilename(sfile));
816            return;
817        }
818
819        boolean switchAfter = false;
820        if (!isEditMode) {
821            setEditMode(true);
822            switchAfter = true;
823        }
824
825        int bSize = 23;
826        // Get InternalFrame border size
827        if (e.getAttribute("border") != null) {
828            bSize = Integer.parseInt((e.getAttribute("border").getValue()));
829        }
830        Element controlPanelElement = e.getChild("ControlPanel");
831        controlPanel.setXml(controlPanelElement);
832        if (((javax.swing.plaf.basic.BasicInternalFrameUI) controlPanel.getUI()).getNorthPane() != null) {
833            ((javax.swing.plaf.basic.BasicInternalFrameUI) controlPanel.getUI()).getNorthPane().setPreferredSize(new Dimension(0, bSize));
834        }
835        Element functionPanelElement = e.getChild("FunctionPanel");
836        functionPanel.setXml(functionPanelElement);
837        if (((javax.swing.plaf.basic.BasicInternalFrameUI) functionPanel.getUI()).getNorthPane() != null) {
838            ((javax.swing.plaf.basic.BasicInternalFrameUI) functionPanel.getUI()).getNorthPane().setPreferredSize(new Dimension(0, bSize));
839        }
840        Element addressPanelElement = e.getChild("AddressPanel");
841        addressPanel.setXml(addressPanelElement);
842        if (((javax.swing.plaf.basic.BasicInternalFrameUI) addressPanel.getUI()).getNorthPane() != null) {
843            ((javax.swing.plaf.basic.BasicInternalFrameUI) addressPanel.getUI()).getNorthPane().setPreferredSize(new Dimension(0, bSize));
844        }
845        Element speedPanelElement = e.getChild("SpeedPanel");
846        if (speedPanelElement != null) { // older throttle configs may not have this element
847            speedPanel.setXml(speedPanelElement);
848            if (((javax.swing.plaf.basic.BasicInternalFrameUI) speedPanel.getUI()).getNorthPane() != null) {
849                ((javax.swing.plaf.basic.BasicInternalFrameUI) speedPanel.getUI()).getNorthPane().setPreferredSize(new Dimension(0, bSize));
850            }
851        }
852
853        List<Element> jinsts = e.getChildren("Jynstrument");
854        if ((jinsts != null) && (jinsts.size() > 0)) {
855            for (Element jinst : jinsts) {
856                JInternalFrame jif = ynstrument(FileUtil.getExternalFilename(jinst.getAttributeValue("JynstrumentFolder")));
857                Element window = jinst.getChild("window");
858                if (jif != null) {
859                    if (window != null) {
860                        WindowPreferences.setPreferences(jif, window);
861                    }
862                    Component[] cmps2 = jif.getContentPane().getComponents();
863                    int j = 0;
864                    while ((j < cmps2.length) && (!(cmps2[j] instanceof Jynstrument))) {
865                        j++;
866                    }
867                    if ((j < cmps2.length) && (cmps2[j] instanceof Jynstrument)) {
868                        ((Jynstrument) cmps2[j]).setXml(jinst);
869                    }
870
871                    jif.repaint();
872                }
873            }
874        }
875        setFrameTitle();
876        if (switchAfter) {
877            setEditMode(false);
878        }
879    }
880
881    /**
882     * setFrameTitle - set the frame title based on type, text and address
883     */
884    public void setFrameTitle() {
885        String winTitle = Bundle.getMessage("ThrottleTitle");
886        if (throttleWindow.getTitleTextType().compareTo("text") == 0) {
887            winTitle = throttleWindow.getTitleText();
888        } else  if ( throttle != null) {
889            String addr  = addressPanel.getCurrentAddress().toString();        
890            if (throttleWindow.getTitleTextType().compareTo("address") == 0) {
891                winTitle = addr;         
892            } else if (throttleWindow.getTitleTextType().compareTo("addressText") == 0) {
893                winTitle = addr + " " + throttleWindow.getTitleText();
894            } else if (throttleWindow.getTitleTextType().compareTo("textAddress") == 0) {
895                winTitle = throttleWindow.getTitleText() + " " + addr;
896            } else if (throttleWindow.getTitleTextType().compareTo("rosterID") == 0) {
897                if ( (addressPanel.getRosterEntry() != null) && (addressPanel.getRosterEntry().getId() != null)
898                        && (addressPanel.getRosterEntry().getId().length() > 0)) {
899                    winTitle = addressPanel.getRosterEntry().getId();
900                } else {
901                    winTitle = addr; // better than nothing in that particular case
902                }
903            }
904        }
905        throttleWindow.setTitle(winTitle);        
906    }
907
908    @Override
909    public void componentHidden(ComponentEvent e) {
910    }
911
912    @Override
913    public void componentMoved(ComponentEvent e) {
914    }
915
916    @Override
917    public void componentResized(ComponentEvent e) {
918//  checkPosition ();
919    }
920
921    @Override
922    public void componentShown(ComponentEvent e) {
923        throttleWindow.setCurrentThrottleFrame(this);
924        if (willSwitch) {
925            setEditMode(this.throttleWindow.isEditMode());
926            repaint();
927        }
928        throttleWindow.updateGUI();
929        // bring addresspanel to front if no allocated throttle
930        if (addressPanel.getThrottle() == null && throttleWindow.isEditMode()) {
931            if (!addressPanel.isVisible()) {
932                addressPanel.setVisible(true);
933            }
934            if (addressPanel.isIcon()) {
935                try {
936                    addressPanel.setIcon(false);
937                } catch (PropertyVetoException ex) {
938                    log.debug("JInternalFrame uniconify, vetoed");
939                }
940            }
941            addressPanel.requestFocus();
942            addressPanel.toFront();
943            try {
944                addressPanel.setSelected(true);
945            } catch (java.beans.PropertyVetoException ex) {
946                log.debug("JInternalFrame selection, vetoed");
947            }
948        }
949    }
950
951    public void saveThrottle() {
952        if (getRosterEntry() != null) {
953            saveThrottle(getDefaultThrottleFolder() + addressPanel.getRosterEntry().getId().trim() + ".xml");
954        } else if (getLastUsedSaveFile() != null) {
955            saveThrottle(getLastUsedSaveFile());
956        }
957    }
958
959    public void saveThrottleAs() {
960        JFileChooser fileChooser = jmri.jmrit.XmlFile.userFileChooser(Bundle.getMessage("PromptXmlFileTypes"), "xml");
961        fileChooser.setCurrentDirectory(new File(getDefaultThrottleFolder()));
962        fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
963        java.io.File file = StoreXmlConfigAction.getFileName(fileChooser);
964        if (file == null) {
965            return;
966        }
967        saveThrottle(file.getAbsolutePath());
968    }
969
970    public void activateNextJInternalFrame() {
971        try {
972            int initialFrame = activeFrame; // avoid infinite loop
973            do {
974                activeFrame = (activeFrame + 1) % NUM_FRAMES;
975                frameList[activeFrame].setSelected(true);
976            } while ((frameList[activeFrame].isClosed() || frameList[activeFrame].isIcon() || (!frameList[activeFrame].isVisible())) && (initialFrame != activeFrame));
977        } catch (PropertyVetoException ex) {
978            log.warn("Exception selecting internal frame:{}", ex.getMessage());
979        }
980    }
981
982    public void activatePreviousJInternalFrame() {
983        try {
984            int initialFrame = activeFrame; // avoid infinite loop
985            do {
986                activeFrame--;
987                if (activeFrame < 0) {
988                    activeFrame = NUM_FRAMES - 1;
989                }
990                frameList[activeFrame].setSelected(true);
991            } while ((frameList[activeFrame].isClosed() || frameList[activeFrame].isIcon() || (!frameList[activeFrame].isVisible())) && (initialFrame != activeFrame));
992        } catch (PropertyVetoException ex) {
993            log.warn("Exception selecting internal frame:{}", ex.getMessage());
994        }
995    }
996
997    @Override
998    public void notifyAddressChosen(LocoAddress l) {
999    }
1000
1001    @Override
1002    public void notifyAddressReleased(LocoAddress la) {
1003        if (throttle == null) {
1004            log.debug("notifyAddressReleased() throttle already null, called for loc {}",la);
1005            return;
1006        }
1007        if (allThrottlesTableModel.getNumberOfEntriesFor((DccLocoAddress) throttle.getLocoAddress()) == 1 )  {
1008            throttleManager.removeListener(throttle.getLocoAddress(), allThrottlesTableModel);
1009        }        
1010        throttle = null;
1011        setLastUsedSaveFile(null);        
1012        setFrameTitle();
1013        throttleWindow.updateGUI(); 
1014        allThrottlesTableModel.fireTableDataChanged();        
1015    }
1016
1017    @Override
1018    public void notifyAddressThrottleFound(DccThrottle t) {
1019        if (throttle != null) {
1020            log.debug("notifyAddressThrottleFound() throttle non null, called for loc {}",t.getLocoAddress());
1021            return;
1022        }
1023        throttle = t;
1024        if ((InstanceManager.getDefault(ThrottlesPreferences.class).isUsingExThrottle())
1025                && (InstanceManager.getDefault(ThrottlesPreferences.class).isAutoLoading()) && (addressPanel != null)) {
1026            if ((addressPanel.getRosterEntry() != null)
1027                    && ((getLastUsedSaveFile() == null) || (getLastUsedSaveFile().compareTo(getDefaultThrottleFolder() + addressPanel.getRosterEntry().getId().trim() + ".xml") != 0))) {
1028                loadThrottle(getDefaultThrottleFolder() + addressPanel.getRosterEntry().getId().trim() + ".xml");
1029                setLastUsedSaveFile(getDefaultThrottleFolder() + addressPanel.getRosterEntry().getId().trim() + ".xml");
1030            } else if ((addressPanel.getRosterEntry() == null)
1031                    && ((getLastUsedSaveFile() == null) || (getLastUsedSaveFile().compareTo(getDefaultThrottleFolder() + addressPanel.getCurrentAddress()+ ".xml") != 0))) {
1032                loadThrottle(getDefaultThrottleFolder() + throttle.getLocoAddress().getNumber() + ".xml");
1033                setLastUsedSaveFile(getDefaultThrottleFolder() + throttle.getLocoAddress().getNumber() + ".xml");
1034            }
1035        } else {
1036            if ((addressPanel != null) && (addressPanel.getRosterEntry() == null)) { // no known roster entry
1037                loadDefaultThrottle();
1038            }
1039        }
1040        setFrameTitle();
1041        throttleWindow.updateGUI();
1042        throttleManager.attachListener(throttle.getLocoAddress(), allThrottlesTableModel);        
1043        allThrottlesTableModel.fireTableDataChanged();
1044    }
1045
1046    
1047    @Override
1048    public void notifyConsistAddressChosen(LocoAddress l) {
1049        notifyAddressChosen(l);
1050    }
1051
1052    
1053    @Override
1054    public void notifyConsistAddressReleased(LocoAddress la) {
1055        notifyAddressReleased(la);
1056    }
1057
1058    @Override
1059    public void notifyConsistAddressThrottleFound(DccThrottle throttle) {
1060        notifyAddressThrottleFound(throttle);
1061    }
1062
1063    public String getLastUsedSaveFile() {
1064        return lastUsedSaveFile;
1065    }
1066
1067    public void setLastUsedSaveFile(String lusf) {
1068        lastUsedSaveFile = lusf;
1069        throttleWindow.updateGUI();
1070    }
1071
1072    // some utilities to turn a component background transparent
1073    public static void setTransparentBackground(JComponent jcomp) {
1074        if (jcomp instanceof JPanel) //OS X: Jpanel components are enough
1075        {
1076            jcomp.setBackground(new Color(0, 0, 0, 0));
1077        }
1078        setTransparentBackground(jcomp.getComponents());
1079    }
1080
1081    public static void setTransparentBackground(Component[] comps) {
1082        for (Component comp : comps) {
1083            try {
1084                if (comp instanceof JComponent) {
1085                    setTransparentBackground((JComponent) comp);
1086                }
1087            } catch (Exception e) {
1088                // Do nothing, just go on
1089            }
1090        }
1091    }
1092
1093// some utilities to turn a component background transparent
1094    public static void setTransparent(JComponent jcomp) {
1095        setTransparent(jcomp, true);
1096    }
1097
1098    public static void setTransparent(JComponent jcomp, boolean transparency) {
1099        if (jcomp instanceof JPanel) { //OS X: Jpanel components are enough
1100            jcomp.setOpaque(!transparency);
1101        }
1102        setTransparent(jcomp.getComponents(), transparency);
1103    }
1104
1105    private static void setTransparent(Component[] comps, boolean transparency) {
1106        for (Component comp : comps) {
1107            try {
1108                if (comp instanceof JComponent) {
1109                    setTransparent((JComponent) comp, transparency);
1110                }
1111            } catch (Exception e) {
1112                // Do nothing, just go on
1113            }
1114        }
1115    }
1116
1117    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ThrottleFrame.class);
1118}