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