001package jmri.jmrit.throttle.implementation;
002
003import java.awt.BorderLayout;
004import java.awt.Component;
005import java.awt.Container;
006import java.awt.Dimension;
007import java.awt.Point;
008import java.awt.Rectangle;
009import java.awt.event.ComponentEvent;
010import java.awt.event.ComponentListener;
011import java.awt.event.ContainerEvent;
012import java.awt.event.ContainerListener;
013import java.beans.PropertyVetoException;
014import java.io.File;
015import java.io.FileNotFoundException;
016import java.io.IOException;
017import java.net.URI;
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021
022import javax.swing.JDesktopPane;
023import javax.swing.JFileChooser;
024import javax.swing.JInternalFrame;
025import javax.swing.JLabel;
026import javax.swing.JPanel;
027import javax.swing.event.InternalFrameAdapter;
028import javax.swing.event.InternalFrameEvent;
029
030import org.jdom2.Element;
031import org.jdom2.JDOMException;
032
033import jmri.DccLocoAddress;
034import jmri.DccThrottle;
035import jmri.InstanceManager;
036import jmri.ThrottleManager;
037import jmri.configurexml.LoadXmlConfigAction;
038import jmri.jmrit.jython.Jynstrument;
039import jmri.jmrit.jython.JynstrumentFactory;
040import jmri.jmrit.roster.RosterEntry;
041import jmri.jmrit.roster.swing.RosterEntrySelectorPanel;
042import jmri.jmrit.throttle.ThrottleFrameManager;
043import jmri.jmrit.throttle.ThrottleWindow;
044import jmri.jmrit.throttle.interfaces.AddressListener;
045import jmri.jmrit.throttle.interfaces.ThrottleControllerUI;
046import jmri.jmrit.throttle.interfaces.ThrottleControllersUIContainer;
047import jmri.jmrit.throttle.panels.ControlPanel;
048import jmri.jmrit.throttle.panels.FunctionButton;
049import jmri.util.FileUtil;
050import jmri.util.iharder.dnd.URIDrop;
051import jmri.util.swing.TransparencyUtils;
052
053/**
054 * The classic JMRI throttle implementation as a JDesktopPane
055 * 
056 * Class naming is bad but kept for backwards compatibility. This is the main class for the throttle UI, it contains the 4 main panels (address, control, function and speed) as JInternalFrames and manages them. It also manages the Jynstruments that can be added to the throttle frame.
057 * 
058 * <hr>
059 * This file is part of JMRI.
060 * <p>
061 * JMRI is free software; you can redistribute it and/or modify it under the
062 * terms of version 2 of the GNU General Public License as published by the Free
063 * Software Foundation. See the "COPYING" file for a copy of this license.
064 * <p>
065 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
066 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
067 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
068 *
069 * @author Lionel Jeanson Copyright 2026
070 *
071 */
072public class ThrottleFrame extends JDesktopPane implements ComponentListener, ThrottleControllerUI {
073
074    private final ThrottleManager throttleManager;
075    private final ThrottleFrameManager throttleFrameManager = InstanceManager.getDefault(ThrottleFrameManager.class);
076
077    private final ThrottleUICore throuic;
078    private ThrottleWindow throttleWindow;
079
080    private static final int ADDRESS_PANEL_INDEX = 0;
081    private static final int CONTROL_PANEL_INDEX = 1;
082    private static final int FUNCTION_PANEL_INDEX = 2;
083    private static final int SPEED_DISPLAY_INDEX = 3;
084    private static final int LOCOICON_INDEX = 4;
085    private static final int CONSIST_FUNCTIONS_INDEX = 5;
086    private static final int NUM_FRAMES = 6;
087    private static final Integer BACKPANEL_LAYER = Integer.MIN_VALUE;
088    private static final Integer PANEL_LAYER_FRAME = 1;
089    private static final Integer PANEL_LAYER_PANEL = 2;
090    
091    private FrameListener frameListener;
092    private JInternalFrame[] frameList;
093    private int activeFrame;
094    private HashMap<Container, JInternalFrame> contentPanes;
095
096    private JInternalFrame controlPanelJIF;
097    private JInternalFrame addressPanelJIF;
098    private JInternalFrame functionPanelJIF;
099    private JInternalFrame speedPanelJIF;
100    private JInternalFrame locoIconJIF;
101    private JInternalFrame consistFunctionsPanelJIF;
102
103    private String title;
104
105    private boolean isEditMode = true;
106    private boolean willSwitch = false;
107
108    public ThrottleFrame(ThrottleWindow tw) {
109        this(tw, InstanceManager.getDefault(ThrottleManager.class));
110    }
111
112    public ThrottleFrame(ThrottleWindow tw, ThrottleManager tm) {
113        super();
114        throttleManager = tm;
115        throttleWindow = tw;
116        throuic = new ThrottleUICore(throttleManager, this);
117        initGUI();
118        throuic.loadDefaultThrottle();
119        throttleFrameManager.getThrottlesListPanel().getTableModel().fireTableStructureChanged();
120    }
121
122    private void initGUI() {
123        setOpaque(false);
124        setDoubleBuffered(true);
125        contentPanes = new HashMap<>();
126        frameListener = new FrameListener();
127
128        controlPanelJIF = new ThrottleJInternalFrameSubControl(Bundle.getMessage("ThrottleMenuViewControlPanel"),throuic.getControlPanel(),true);
129        controlPanelJIF.addInternalFrameListener(frameListener);
130        addressPanelJIF = new ThrottleJInternalFrameSubControl(Bundle.getMessage("ThrottleMenuViewAddressPanel"),throuic.getAddressPanel(),true);
131        addressPanelJIF.addInternalFrameListener(frameListener);
132        functionPanelJIF = new ThrottleJInternalFrameSubControl(Bundle.getMessage("ThrottleMenuViewFunctionPanel"),throuic.getFunctionPanel(),true);
133        functionPanelJIF.addInternalFrameListener(frameListener);
134
135        controlPanelJIF.pack();
136        addressPanelJIF.pack();
137        functionPanelJIF.pack();
138
139        // assumes button width of 54, height of 30 (set in class FunctionButton) with
140        // horiz and vert gaps of 5 each (set in FunctionPanel class)
141        // with 4 buttons across and 6 rows high
142        int width = 4 * (FunctionButton.getButtonWidth()) + 2 * 4 * 5 + 10;   
143        int height = 6 * (FunctionButton.getButtonHeight()) + 2 * 6 * 5 + 20; 
144        
145        functionPanelJIF.setSize(width, height);
146        functionPanelJIF.setLocation(controlPanelJIF.getWidth(), 0);
147
148        if (addressPanelJIF.getWidth()<functionPanelJIF.getWidth()) {
149            addressPanelJIF.setSize(functionPanelJIF.getWidth(),addressPanelJIF.getHeight());
150        }
151        addressPanelJIF.setLocation(controlPanelJIF.getWidth(), functionPanelJIF.getHeight());
152
153        if (controlPanelJIF.getHeight() < functionPanelJIF.getHeight() + addressPanelJIF.getHeight()) {
154            controlPanelJIF.setSize(controlPanelJIF.getWidth(), functionPanelJIF.getHeight() + addressPanelJIF.getHeight());
155        }
156        if (controlPanelJIF.getHeight() > functionPanelJIF.getHeight() + addressPanelJIF.getHeight()) {
157            addressPanelJIF.setSize(addressPanelJIF.getWidth(), controlPanelJIF.getHeight() - functionPanelJIF.getHeight());
158        }
159//        if (functionPanelJIF.getWidth() < addressPanelJIF.getWidth()) {
160//            functionPanelJIF.setSize(addressPanelJIF.getWidth(), functionPanelJIF.getHeight());
161//        }
162
163        setPreferredSize(new Dimension(
164                Math.max(controlPanelJIF.getWidth() + functionPanelJIF.getWidth(), controlPanelJIF.getWidth() + addressPanelJIF.getWidth()),
165                Math.max(addressPanelJIF.getHeight() + functionPanelJIF.getHeight(), controlPanelJIF.getHeight()) ));
166
167        add(controlPanelJIF, PANEL_LAYER_FRAME);
168        add(functionPanelJIF, PANEL_LAYER_FRAME);
169        add(addressPanelJIF, PANEL_LAYER_FRAME);
170
171        addComponentListener(throuic.getBackgroundPanel()); // backgroudPanel warned when desktop resized
172        add(throuic.getBackgroundPanel(), BACKPANEL_LAYER);
173
174        addComponentListener(this);  // to force sub windows repositionning
175
176        frameList = new JInternalFrame[NUM_FRAMES];
177        frameList[ADDRESS_PANEL_INDEX] = addressPanelJIF;
178        frameList[CONTROL_PANEL_INDEX] = controlPanelJIF;
179        frameList[FUNCTION_PANEL_INDEX] = functionPanelJIF;
180        activeFrame = ADDRESS_PANEL_INDEX;
181
182        // #JYNSTRUMENT# Bellow prepare drag'n drop receptacle:
183        new URIDrop(throuic.getBackgroundPanel(), uris -> {
184                if (isEditMode) {
185                    for (URI uri : uris ) {
186                        ynstrument(new File(uri).getPath());
187                    }
188                }
189            });
190            
191        try {
192            addressPanelJIF.setSelected(true);
193        } catch (PropertyVetoException ex) {
194            log.error("Error selecting InternalFrame: {}", ex.getMessage());
195        }
196
197    }
198    
199    private JInternalFrame createSpeedPanelJIF() {
200        if (speedPanelJIF != null) {
201            return speedPanelJIF;
202        }
203        speedPanelJIF = new ThrottleJInternalFrameSubControl(Bundle.getMessage("ThrottleMenuViewSpeedPanel"),throuic.getSpeedPanel(),false);
204        speedPanelJIF.addInternalFrameListener(frameListener);
205        speedPanelJIF.pack();
206        speedPanelJIF.setSize(addressPanelJIF.getWidth() + controlPanelJIF.getWidth(), addressPanelJIF.getHeight() / 2);
207        speedPanelJIF.setLocation(0, controlPanelJIF.getHeight());        
208        add(speedPanelJIF, PANEL_LAYER_FRAME);
209        frameList[SPEED_DISPLAY_INDEX] = speedPanelJIF;
210        return speedPanelJIF;
211    }
212
213    private JInternalFrame createLocoIconPanelJIF() {
214        if (locoIconJIF != null) {
215            return locoIconJIF;
216        }
217        locoIconJIF = new ThrottleJInternalFrameSubControl(Bundle.getMessage("ThrottleMenuViewLocoIconPanel"),throuic.getLocoIconPanel(),false);
218        locoIconJIF.addInternalFrameListener(frameListener);
219        locoIconJIF.pack();
220        locoIconJIF.setSize(functionPanelJIF.getWidth(), addressPanelJIF.getHeight() / 2);
221        locoIconJIF.setLocation(controlPanelJIF.getWidth()+functionPanelJIF.getWidth(), 0 );
222        add(locoIconJIF, PANEL_LAYER_FRAME);
223        frameList[LOCOICON_INDEX] = locoIconJIF;
224        return locoIconJIF;
225    }
226
227    private JInternalFrame createConsistFunctionsPanelJIF() {
228        if (consistFunctionsPanelJIF != null) {
229            return consistFunctionsPanelJIF;
230        }
231        consistFunctionsPanelJIF = new ThrottleJInternalFrameSubControl(Bundle.getMessage("ThrottleMenuViewConsistFunctionsPanel"),throuic.getConsistFunctionsPanel(),false);
232        consistFunctionsPanelJIF.addInternalFrameListener(frameListener);
233        consistFunctionsPanelJIF.pack();
234        consistFunctionsPanelJIF.setSize(functionPanelJIF.getWidth(), functionPanelJIF.getHeight() / 2);
235        consistFunctionsPanelJIF.setLocation(functionPanelJIF.getLocation().x, functionPanelJIF.getLocation().y+functionPanelJIF.getHeight() ); // bellow the regular function panel
236        add(consistFunctionsPanelJIF, PANEL_LAYER_FRAME);
237        frameList[CONSIST_FUNCTIONS_INDEX] = consistFunctionsPanelJIF;
238        return consistFunctionsPanelJIF;        
239    }
240
241    public JInternalFrame getControlPanelJIF() {
242        return controlPanelJIF;
243    }
244
245    public JInternalFrame getAddressPanelJIF() {
246        return addressPanelJIF;
247    }
248
249    public JInternalFrame getFunctionPanelJIF() {
250        return functionPanelJIF;
251    }
252
253    @Override
254    public void updateGUI() {
255        throttleWindow.updateGUI();        
256    }
257
258    public void setAddressPanelVisible(boolean visible) {
259        addressPanelJIF.setVisible(visible);
260        if (visible) {
261            checkPosition(addressPanelJIF);
262        }
263    }
264
265    public void setControlPanelVisible(boolean visible) {
266        controlPanelJIF.setVisible(visible);
267        if (visible) {
268            checkPosition(controlPanelJIF);
269        }
270    }
271
272    public void setFunctionPanelVisible(boolean visible) {
273        functionPanelJIF.setVisible(visible);
274        if (visible) {
275            checkPosition(functionPanelJIF);
276        }
277    }
278
279    public void setSpeedPanelVisible(boolean visible) {
280        if (speedPanelJIF == null) {
281            if (visible) {
282                speedPanelJIF = createSpeedPanelJIF();
283            } else {
284                return;
285            }
286        }    
287        speedPanelJIF.setVisible(visible);
288        if (visible) {
289            checkPosition(speedPanelJIF);
290        }
291    }
292
293    public void setLocoIconPanelVisible(boolean visible) {
294        if (locoIconJIF == null) {
295            if (visible) {
296                locoIconJIF = createLocoIconPanelJIF();
297            }else {
298                return;
299            }
300        }        
301        locoIconJIF.setVisible(visible);
302        if (visible) {
303            checkPosition(locoIconJIF);
304        }
305    }
306
307    public void setConsistFunctionsPanelVisible(boolean visible) {
308        if (consistFunctionsPanelJIF == null) {
309            if (visible) {
310                consistFunctionsPanelJIF = createConsistFunctionsPanelJIF();
311            } else {
312                return;
313            }
314        }
315        consistFunctionsPanelJIF.setVisible(visible);
316        if (visible) {
317            checkPosition(consistFunctionsPanelJIF);
318        }
319    }
320
321    public void resetFunctionPanelButton() {
322        throuic.getFunctionPanel().resetFnButtons();
323        throuic.getFunctionPanel().setEnabled();
324    }
325
326    /**
327     * Handle my own destruction.
328     * <ol>
329     * <li> dispose of sub windows.
330     * <li> notify my manager of my demise.
331     * </ol>
332     */
333    public void dispose() {
334        log.debug("Disposing");
335        URIDrop.remove(throuic.getBackgroundPanel());
336        throuic.dispose();        
337    }
338
339    @Override
340    public ThrottleWindow getThrottleControllersContainer() {
341        return throttleWindow;
342    }
343    
344    @Override
345    public void setThrottleControllersContainer(ThrottleControllersUIContainer tw) {
346        if (tw instanceof ThrottleWindow) {
347            throttleWindow = (ThrottleWindow) tw;
348        } else {
349            throw new IllegalArgumentException("Unable to set throttle controllers container, provided container is not an instance of ThrottleWindow");
350        }        
351    }
352
353    @Override
354    public void toFront() {
355        if (throttleWindow == null) {
356            return;
357        }
358        throttleWindow.toFront(title);
359    }
360
361    @Override
362    public void setRosterEntry(RosterEntry re) {
363        throuic.setRosterEntry(re);
364    }
365
366    @Override
367    public RosterEntry getRosterEntry() {
368        return throuic.getRosterEntry();
369    }
370
371    @Override
372    public RosterEntry getFunctionRosterEntry() {   
373        return throuic.getFunctionRosterEntry();                
374    }
375
376    @Override
377    public void setAddress(DccLocoAddress la) {
378        throuic.setAddress(la);
379    }
380
381    @Override
382    public DccLocoAddress getAddress() {
383        return throuic.getAddress();
384    }
385
386    @Override
387    public void setConsistAddress(DccLocoAddress la) {
388        throuic.setConsistAddress(la);
389    }
390
391    @Override
392    public void dispatchAddress() {
393        throuic.getAddressPanel().dispatchAddress();
394    }
395
396    @Override
397    public boolean isUsingAddress(DccLocoAddress la) {                    
398        if ( getThrottle() != null && 
399                ( la.equals( throuic.getAddressPanel().getCurrentAddress()) || la.equals( throuic.getAddressPanel().getConsistAddress()) ) ) {
400            return true;
401        }
402        return false;
403    }
404
405    @Override
406    public void eStop() {
407        throuic.eStop();
408    }
409
410    @Override
411    public boolean isRunning() {
412        return ((getThrottle() != null) && (getThrottle().getSpeedSetting() > 0));
413    }
414
415    @Override
416    public boolean isActive() {
417        return ( (getThrottle() != null) && ( (getThrottle().getSpeedSetting() > 0) || (throuic.hasActiveFunction())));
418    }
419
420    @Override
421    public DccThrottle getThrottle() {
422        return throuic.getThrottle();  
423    }
424
425    @Override
426    public DccThrottle getFunctionThrottle() {
427        return throuic.getFunctionThrottle();        
428    }
429
430    @Override
431    public JLabel getLabel() {
432        return new JLabel(throuic.getLocoIconPanel().getDescription(), throuic.getLocoIconPanel().getIcon(), JLabel.CENTER);
433    }
434
435    @Override
436    public boolean isSpeedDisplayContinuous() {
437        return throuic.getControlPanel().getDisplaySlider() == ControlPanel.SLIDERDISPLAYCONTINUOUS;
438    }
439
440    public void forceAddressPanelSelected() {
441        try {
442            addressPanelJIF.setSelected(true);
443        } catch (PropertyVetoException ex) {
444            log.warn("Unable to force selection of address panel", ex);
445        }
446    }
447
448    public boolean isAddressPanelVisible() {
449        return addressPanelJIF.isVisible();
450    }
451
452    public boolean isControlPanelVisible() {
453        return controlPanelJIF.isVisible();
454    }
455
456    public boolean isFunctionPanelVisible() {
457        return functionPanelJIF.isVisible();
458    }
459
460    public boolean isSpeedPanelVisible() {
461        return ((speedPanelJIF != null) && (speedPanelJIF.isVisible()));
462    }
463
464    public boolean isLocoIconPanelVisible() {
465        return ((locoIconJIF != null) && (locoIconJIF.isVisible()));
466    }
467
468    public boolean isConsistFunctionsPanelVisible() {
469        return ((consistFunctionsPanelJIF != null) && (consistFunctionsPanelJIF.isVisible()));
470    }
471
472    public boolean isKnownLastUsedSaveFile() {
473        return (throuic.getLastUsedSaveFile() != null);
474    }
475
476    public boolean isKnownRosterEntry() {
477        return ( throuic.getRosterEntry() != null);
478    }
479
480    public void setTitle(String s) {
481        title = s;     
482    }
483
484    public void saveRosterChanges() {
485        throuic.saveRosterChanges();
486    }
487
488    @Override
489    public RosterEntrySelectorPanel getRosterEntrySelector() {
490        return throuic.getAddressPanel().getRosterEntrySelector();
491    }
492
493    @Override
494    public void addAddressListener(AddressListener l) {
495        throuic.getAddressPanel().addAddressListener(l);
496    }
497
498    @Override
499    public void removeAddressListener(AddressListener l) {
500        throuic.getAddressPanel().removeAddressListener(l);
501    }
502
503    /**
504     * Sets the location of a throttle frame on the screen according to x and y
505     * coordinates
506     *
507     * @see java.awt.Component#setLocation(int, int)
508     */
509    @Override
510    public void setLocation(int x, int y) {
511        if (throttleWindow == null) {
512            return;
513        }
514        throttleWindow.setLocation(new Point(x, y));
515    }    
516
517    @Override
518    public void componentResized(ComponentEvent e) {
519        //  checkPosition ();
520    }
521
522    @Override
523    public void componentMoved(ComponentEvent e) {
524        // do nothing
525    }
526
527    @Override
528    public void componentShown(ComponentEvent e) {
529        throttleWindow.setCurrentThrottleFrame(this);
530        if (willSwitch) {
531            setEditMode(this.throttleWindow.isEditMode());
532            repaint();
533        }
534        throttleWindow.updateGUI();
535        // bring addresspanel to front if no allocated throttle
536        if (getThrottle() == null && throttleWindow.isEditMode()) {
537            if (!addressPanelJIF.isVisible()) {
538                setAddressPanelVisible(true);
539            }
540            if (addressPanelJIF.isIcon()) {
541                try {
542                    addressPanelJIF.setIcon(false);
543                } catch (PropertyVetoException ex) {
544                    log.debug("JInternalFrame uniconify, vetoed");
545                }
546            }
547            addressPanelJIF.requestFocus();
548            addressPanelJIF.toFront();
549            try {
550                addressPanelJIF.setSelected(true);
551            } catch (java.beans.PropertyVetoException ex) {
552                log.debug("JInternalFrame selection, vetoed");
553            }
554        }
555    }
556
557    @Override
558    public void componentHidden(ComponentEvent e) {
559        // do nothing
560    }
561
562    // #JYNSTRUMENT# here instantiate the Jynstrument, put it in a component, initialize the context and start it
563    public JInternalFrame ynstrument(String path) {
564        if (path == null) {
565            return null;
566        }
567        Jynstrument it = JynstrumentFactory.createInstrument(path, this); // everything is there
568        if (it == null) {
569            log.error("Error while creating Jynstrument {}", path);
570            return null;
571        }
572        TransparencyUtils.setTransparentBackgroundRec(it);
573        JInternalFrame newiFrame = new JInternalFrame(it.getClassName());
574        newiFrame.add(it);
575        newiFrame.addInternalFrameListener(frameListener);
576        newiFrame.setDoubleBuffered(true);
577        newiFrame.setResizable(true);
578        newiFrame.setClosable(true);
579        newiFrame.setIconifiable(true);
580        newiFrame.getContentPane().addContainerListener(new ContainerListener() {
581            @Override
582            public void componentAdded(ContainerEvent e) {
583            }
584
585            @Override
586            public void componentRemoved(ContainerEvent e) {
587                Container c = e.getContainer();
588                while ((!(c instanceof JInternalFrame)) && (!(c instanceof TranslucentJPanel))) {
589                    c = c.getParent();
590                }
591                c.setVisible(false);
592                remove(c);
593                repaint();
594            }
595        });
596        newiFrame.pack();
597        add(newiFrame, PANEL_LAYER_FRAME);
598        newiFrame.setVisible(true);
599        return newiFrame;
600    }
601
602    // make sure components are inside this frame bounds
603    private void checkPosition(Component comp) {
604        if ((this.getWidth() < 1) || (this.getHeight() < 1)) {
605            return;
606        }
607
608        Rectangle pos = comp.getBounds();
609        if (pos.width > this.getWidth()) { // Component largest than container
610            pos.width = this.getWidth() - 2;
611            pos.x = 1;
612        }
613        if (pos.x + pos.width > this.getWidth()) { // Component to large        
614            pos.x = this.getWidth() - pos.width - 1;
615        }
616        if (pos.x < 0) { // Component to far on the left
617            pos.x = 1;
618        }
619
620        if (pos.height > this.getHeight()) { // Component higher than container
621            pos.height = this.getHeight() - 2;
622            pos.y = 1;
623        }
624        if (pos.y + pos.height > this.getHeight()) { // Component to low
625            pos.y = this.getHeight() - pos.height - 1;
626        }
627        if (pos.y < 0) { // Component to high
628            pos.y = 1;
629        }
630        comp.setBounds(pos);
631    }
632
633    public void makeAllComponentsInBounds() {
634        Component[] cmps = getComponents();
635        for (Component cmp : cmps) {
636            checkPosition(cmp);
637        }
638    }
639
640    private void translude(JInternalFrame jif) {
641        Dimension cpSize = jif.getContentPane().getSize();
642        Point cpLoc = jif.getContentPane().getLocationOnScreen();
643        TranslucentJPanel pane = new TranslucentJPanel();
644        pane.setLayout(new BorderLayout());
645        contentPanes.put(pane, jif);
646        pane.add(jif.getContentPane(), BorderLayout.CENTER);
647        TransparencyUtils.setOpacityRec(pane, true);
648        jif.setContentPane(new JPanel());
649        jif.setVisible(false);
650        Point loc = new Point(cpLoc.x - this.getLocationOnScreen().x, cpLoc.y - this.getLocationOnScreen().y);
651        add(pane, PANEL_LAYER_PANEL);
652        pane.setLocation(loc);
653        pane.setSize(cpSize);
654    }
655
656    private void playRendering() {
657        Component[] cmps = getComponentsInLayer(PANEL_LAYER_FRAME);
658        contentPanes = new HashMap<>();
659        for (Component cmp : cmps) {
660            if ((cmp instanceof JInternalFrame) && (cmp.isVisible())) {
661                translude((JInternalFrame)cmp);
662            }
663        }
664    }
665
666    private void editRendering() {
667        Component[] cmps = getComponentsInLayer(PANEL_LAYER_PANEL);
668        for (Component cmp : cmps) {
669            if (cmp instanceof JPanel) {
670                JPanel pane = (JPanel) cmp;
671                JInternalFrame jif = contentPanes.get(pane);
672                jif.setContentPane((Container) pane.getComponent(0));
673                TransparencyUtils.setOpacityRec(jif, false);
674                jif.setVisible(true);
675                remove(pane);
676            }
677        }
678    }
679
680    public void setEditMode(boolean mode) {
681        if (mode == isEditMode)
682            return;
683        if (isVisible()) {
684            if (!mode) {
685                playRendering();
686            } else {
687                editRendering();
688            }
689            isEditMode = mode;
690            willSwitch = false;
691        } else {
692            willSwitch = true;
693        }
694        throttleWindow.updateGUI();
695    }
696
697    public boolean getEditMode() {
698        return isEditMode;
699    }
700
701    public void activateNextJInternalFrame() {
702        try {
703            int initialFrame = activeFrame; // avoid infinite loop
704            do {
705                activeFrame = (activeFrame + 1) % NUM_FRAMES;
706                frameList[activeFrame].setSelected(true);
707            } while ((frameList[activeFrame]==null || frameList[activeFrame].isClosed() || frameList[activeFrame].isIcon() || (!frameList[activeFrame].isVisible())) && (initialFrame != activeFrame));
708        } catch (PropertyVetoException ex) {
709            log.warn("Exception selecting internal frame:{}", ex.getMessage());
710        }
711    }
712
713    public void activatePreviousJInternalFrame() {
714        try {
715            int initialFrame = activeFrame; // avoid infinite loop
716            do {
717                activeFrame--;
718                if (activeFrame < 0) {
719                    activeFrame = NUM_FRAMES - 1;
720                }
721                frameList[activeFrame].setSelected(true);
722            } while ((frameList[activeFrame]==null || frameList[activeFrame].isClosed() || frameList[activeFrame].isIcon() || (!frameList[activeFrame].isVisible())) && (initialFrame != activeFrame));
723        } catch (PropertyVetoException ex) {
724            log.warn("Exception selecting internal frame:{}", ex.getMessage());
725        }
726    }
727
728    /**
729     * setFrameTitle - set the frame title based on type, text and address
730     * 
731     */
732    @Override
733    public void updateFrameTitle() {
734        String winTitle = Bundle.getMessage("ThrottleTitle");
735        if (throttleWindow.getTitleTextType().compareTo("text") == 0) {
736            winTitle = throttleWindow.getTitleText();
737        } else  if ( getThrottle() != null) {
738            String addr  = throuic.getAddressPanel().getCurrentAddress().toString();        
739            if (throttleWindow.getTitleTextType().compareTo("address") == 0) {
740                winTitle = addr;         
741            } else if (throttleWindow.getTitleTextType().compareTo("addressText") == 0) {
742                winTitle = addr + " " + throttleWindow.getTitleText();
743            } else if (throttleWindow.getTitleTextType().compareTo("textAddress") == 0) {
744                winTitle = throttleWindow.getTitleText() + " " + addr;
745            } else if (throttleWindow.getTitleTextType().compareTo("rosterID") == 0) {
746                if ( (throuic.getRosterEntry() != null) && (throuic.getRosterEntry().getId() != null)
747                        && (throuic.getRosterEntry().getId().length() > 0)) {
748                    winTitle = throuic.getRosterEntry().getId();
749                } else {
750                    winTitle = addr; // better than nothing in that particular case
751                }
752            }
753        }
754        throttleWindow.setTitle(winTitle);        
755    }
756
757    public Element getXmlFile() {
758        if (throuic.getLastUsedSaveFile() == null) { // || (getRosterEntry()==null))
759            return null;
760        }
761        Element me = new Element("ThrottleFrame");
762        me.setAttribute("ThrottleXMLFile", FileUtil.getPortableFilename(throuic.getLastUsedSaveFile()));
763        return me;
764    }
765
766    public void setXml(Element e) {
767        if (e == null) {
768            return;
769        }
770
771        String sfile = e.getAttributeValue("ThrottleXMLFile");
772        if (sfile != null) {
773            loadThrottleFile(FileUtil.getExternalFilename(sfile));
774            return;
775        }
776
777        boolean switchAfter = false;
778        if (!isEditMode) {
779            setEditMode(true);
780            switchAfter = true;
781        }
782
783        int bSize = 23;
784        // Get InternalFrame border size
785        if (e.getAttribute("border") != null) {
786            bSize = Integer.parseInt((e.getAttribute("border").getValue()));
787        }
788        if (((javax.swing.plaf.basic.BasicInternalFrameUI) controlPanelJIF.getUI()).getNorthPane() != null) {
789            ((javax.swing.plaf.basic.BasicInternalFrameUI) controlPanelJIF.getUI()).getNorthPane().setPreferredSize(new Dimension(0, bSize));
790        }
791        if (((javax.swing.plaf.basic.BasicInternalFrameUI) functionPanelJIF.getUI()).getNorthPane() != null) {
792            ((javax.swing.plaf.basic.BasicInternalFrameUI) functionPanelJIF.getUI()).getNorthPane().setPreferredSize(new Dimension(0, bSize));
793        }
794        if (((javax.swing.plaf.basic.BasicInternalFrameUI) addressPanelJIF.getUI()).getNorthPane() != null) {
795            ((javax.swing.plaf.basic.BasicInternalFrameUI) addressPanelJIF.getUI()).getNorthPane().setPreferredSize(new Dimension(0, bSize));
796        }
797
798        setWindowXML(e.getChild("AddressPanel"), addressPanelJIF);
799        setWindowXML(e.getChild("ControlPanel"), controlPanelJIF);        
800        setWindowXML(e.getChild("FunctionPanel"), functionPanelJIF);
801        
802        Element child = e.getChild("SpeedPanel");
803        if (child != null) {
804            createSpeedPanelJIF();
805            if (((javax.swing.plaf.basic.BasicInternalFrameUI) speedPanelJIF.getUI()).getNorthPane() != null) {
806                ((javax.swing.plaf.basic.BasicInternalFrameUI) speedPanelJIF.getUI()).getNorthPane().setPreferredSize(new Dimension(0, bSize));
807            }
808            setWindowXML(child, speedPanelJIF);
809        } else if (speedPanelJIF != null) {
810            speedPanelJIF.setVisible(false);
811        }
812        child = e.getChild("LocoIconPanel");
813        if (child != null) {
814            createLocoIconPanelJIF();
815            if (((javax.swing.plaf.basic.BasicInternalFrameUI) locoIconJIF.getUI()).getNorthPane() != null) {
816                ((javax.swing.plaf.basic.BasicInternalFrameUI) locoIconJIF.getUI()).getNorthPane().setPreferredSize(new Dimension(0, bSize));
817            }
818            setWindowXML(child, locoIconJIF);
819        } else if (locoIconJIF != null) {
820            locoIconJIF.setVisible(false);
821        }
822        child = e.getChild("ConsistFunctionsPanel");
823        if (child != null) {
824            createConsistFunctionsPanelJIF();
825            if (((javax.swing.plaf.basic.BasicInternalFrameUI) consistFunctionsPanelJIF.getUI()).getNorthPane() != null) {
826                ((javax.swing.plaf.basic.BasicInternalFrameUI) consistFunctionsPanelJIF.getUI()).getNorthPane().setPreferredSize(new Dimension(0, bSize));
827            }
828            setWindowXML(child, consistFunctionsPanelJIF);
829        } else if (consistFunctionsPanelJIF != null) {
830            consistFunctionsPanelJIF.setVisible(false);
831        }
832        
833        List<Element> jinsts = e.getChildren("Jynstrument");
834        if ((jinsts != null) && (jinsts.size() > 0)) {
835            for (Element jinst : jinsts) {
836                JInternalFrame jif = ynstrument(FileUtil.getExternalFilename(jinst.getAttributeValue("JynstrumentFolder")));
837                Element window = jinst.getChild("window");
838                if (jif != null) {
839                    if (window != null) {
840                        WindowPreferences.setPreferences(jif, window);
841                    }
842                    Component[] cmps2 = jif.getContentPane().getComponents();
843                    int j = 0;
844                    while ((j < cmps2.length) && (!(cmps2[j] instanceof Jynstrument))) {
845                        j++;
846                    }
847                    if ((j < cmps2.length) && (cmps2[j] instanceof Jynstrument)) {
848                        ((Jynstrument) cmps2[j]).setXml(jinst);
849                    }
850
851                    jif.repaint();
852                }
853            }
854        }
855        updateFrameTitle();
856        if (switchAfter) {
857            setEditMode(false);
858        }
859    }
860
861    private void setWindowXML(Element e, JInternalFrame jif) {
862        if (e == null || jif == null) {
863            return;
864        }
865        Element window = e.getChild("window");
866        if (window == null) { 
867            return;
868        }
869        WindowPreferences.setPreferences(jif, window);                    
870    }
871
872    public Element getXml() {
873        boolean switchAfter = false;
874        if (!isEditMode) {
875            setEditMode(true);
876            switchAfter = true;
877        }
878
879        Element me = new Element("ThrottleFrame");
880
881        if (((javax.swing.plaf.basic.BasicInternalFrameUI) controlPanelJIF.getUI()).getNorthPane() != null) {
882            Dimension bDim = ((javax.swing.plaf.basic.BasicInternalFrameUI) controlPanelJIF.getUI()).getNorthPane().getPreferredSize();
883            me.setAttribute("border", Integer.toString(bDim.height));
884        }
885
886        // save throttle regular inner frames
887        ArrayList<Element> children = new ArrayList<>(6);
888        ArrayList<Element> toRemove = new ArrayList<>(3);
889        throuic.getXml(children);
890        for (Element child : children) { // this is ugly but preserves backward comptabilility
891            if (child.getName().equals("ControlPanel")) {
892               addWindowXML(child, controlPanelJIF);
893            }
894            if (child.getName().equals("FunctionPanel")) {
895               addWindowXML(child, functionPanelJIF);
896            }
897            if (child.getName().equals("AddressPanel")) {
898               addWindowXML(child, addressPanelJIF);
899            }
900            // we tag null or not visible inner windows as to remove from the xml data, this will avoid reinstantiating them on load
901            if (child.getName().equals("SpeedPanel")){
902                if ((speedPanelJIF != null) && (speedPanelJIF.isVisible())) {
903                    addWindowXML(child, speedPanelJIF);
904                } else if (child.getAttributes().isEmpty() && child.getChildren().isEmpty()) {
905                    toRemove.add(child);
906                }                
907            }
908            if (child.getName().equals("LocoIconPanel")) {
909                if ((locoIconJIF != null) && (locoIconJIF.isVisible())) {
910                    addWindowXML(child, locoIconJIF);
911                } else if (child.getAttributes().isEmpty() && child.getChildren().isEmpty()) {
912                    toRemove.add(child);
913                }  
914            }
915            if (child.getName().equals("ConsistFunctionsPanel")) {
916                if ((consistFunctionsPanelJIF != null) && (consistFunctionsPanelJIF.isVisible())) {
917                    addWindowXML(child, consistFunctionsPanelJIF);
918                } else if (child.getAttributes().isEmpty() && child.getChildren().isEmpty()) {
919                    toRemove.add(child);
920                }  
921            }
922        }
923        // remove tagged elements
924        for (Element child : toRemove) {
925            children.remove(child);
926        }
927        // Save Jynstruments
928        Component[] cmps = getComponents();
929        for (Component cmp : cmps) {
930            try {
931                if (cmp instanceof JInternalFrame) {
932                    Component[] cmps2 = ((JInternalFrame) cmp).getContentPane().getComponents();
933                    int j = 0;
934                    while ((j < cmps2.length) && (!(cmps2[j] instanceof Jynstrument))) {
935                        j++;
936                    }
937                    if ((j < cmps2.length) && (cmps2[j] instanceof Jynstrument)) {
938                        Jynstrument jyn = (Jynstrument) cmps2[j];
939                        Element elt = new Element("Jynstrument");
940                        elt.setAttribute("JynstrumentFolder", FileUtil.getPortableFilename(jyn.getFolder()));
941                        ArrayList<Element> jychildren = new ArrayList<>(1);
942                        jychildren.add(WindowPreferences.getPreferences((JInternalFrame) cmp));
943                        Element je = jyn.getXml();
944                        if (je != null) {
945                            jychildren.add(je);
946                        }
947                        elt.setContent(jychildren);
948                        children.add(elt);
949                    }
950                }
951            } catch (Exception ex) {
952                log.debug("Got exception (no panic) {}", ex.getMessage());
953            }
954        }
955        me.setContent(children);
956        if (switchAfter) {
957            setEditMode(false);
958        }
959        return me;
960    }
961
962    private void addWindowXML(Element e, JInternalFrame jif) {
963        if (e == null || jif == null) {
964            return;
965        }
966        java.util.ArrayList<Element> children = new java.util.ArrayList<Element>(1);        
967        children.add(WindowPreferences.getPreferences(jif));        
968        e.addContent(0,children); // has to be the first one as per DTD
969    }
970
971    public void saveThrottle() {
972        throuic.saveThrottle(getXml());
973    }
974
975    public void saveThrottleAs() {
976        throuic.saveThrottleAs(getXml());
977    }    
978
979    @Override
980    public void loadThrottleFile(String sfile) {
981        if (sfile == null) { // null file, ask user to select one
982            JFileChooser fileChooser = jmri.jmrit.XmlFile.userFileChooser(Bundle.getMessage("PromptXmlFileTypes"), "xml");
983            fileChooser.setCurrentDirectory(new File(ThrottleUICore.getDefaultThrottleFolder()));
984            fileChooser.setDialogType(JFileChooser.OPEN_DIALOG);
985            java.io.File file = LoadXmlConfigAction.getFile(fileChooser, this);
986            if (file == null) {
987                return ;
988            }
989        }
990        boolean switchAfter = false;
991        if (!isEditMode) {
992            setEditMode(true);
993            switchAfter = true;
994        }
995        // close all existing Jynstruments
996        Component[] cmps = getComponents();
997        for (Component cmp : cmps) {
998            try {
999                if (cmp instanceof JInternalFrame) {
1000                    JInternalFrame jyf = (JInternalFrame) cmp;
1001                    Component[] cmps2 = jyf.getContentPane().getComponents();
1002                    for (Component cmp2 : cmps2) {
1003                        if (cmp2 instanceof Jynstrument) {
1004                            ((Jynstrument) cmp2).exit();
1005                            jyf.dispose();
1006                        }
1007                    }
1008                }
1009            } catch (Exception ex) {
1010                log.debug("Got exception (no panic) {}", ex.getMessage());
1011            }
1012        }
1013
1014        try {
1015            Element conf = throuic.loadThrottle(sfile);
1016            setXml(conf);
1017        } catch (FileNotFoundException ex) {
1018            // Don't show error dialog if file is not found
1019            log.debug("Loading throttle exception: {}", ex.getMessage());
1020            log.debug("Tried loading throttle file \"{}\" , reverting to default, if any", sfile);
1021            throuic.loadDefaultThrottle(); // revert to loading default one
1022        } catch (NullPointerException | IOException | JDOMException ex) {
1023            log.debug("Loading throttle exception: {}", ex.getMessage());
1024            log.debug("Tried loading throttle file \"{}\" , reverting to default, if any", sfile);
1025            jmri.configurexml.ConfigXmlManager.creationErrorEncountered(
1026                    null, "parsing file " + sfile,
1027                    "Parse error", null, null, ex);
1028            throuic.loadDefaultThrottle(); // revert to loading default one
1029        }
1030        //     checkPosition();
1031        if (switchAfter) {
1032            setEditMode(false);
1033        }
1034    }
1035
1036   /**
1037     * An extension of InternalFrameAdapter for listening to the closing of of
1038     * this frame's internal frames.
1039     *
1040     * @author glen
1041     */
1042    class FrameListener extends InternalFrameAdapter {
1043
1044        /**
1045         * Listen for the closing of an internal frame and set the "View" menu
1046         * appropriately. Then hide the closing frame
1047         *
1048         * @param e The InternalFrameEvent leading to this action
1049         */
1050        @Override
1051        public void internalFrameClosing(InternalFrameEvent e) {
1052            if (e.getSource() == controlPanelJIF) {
1053                throttleWindow.getViewControlPanel().setSelected(false);
1054                controlPanelJIF.setVisible(false);
1055            } else if (e.getSource() == addressPanelJIF) {
1056                throttleWindow.getViewAddressPanel().setSelected(false);
1057                addressPanelJIF.setVisible(false);
1058            } else if (e.getSource() == functionPanelJIF) {
1059                throttleWindow.getViewFunctionPanel().setSelected(false);
1060                functionPanelJIF.setVisible(false);
1061            } else if (e.getSource() == speedPanelJIF) {
1062                throttleWindow.getViewSpeedPanel().setSelected(false);
1063                speedPanelJIF.setVisible(false);
1064            } else if (e.getSource() == locoIconJIF) {
1065                throttleWindow.getViewLocoIconPanel().setSelected(false);
1066                locoIconJIF.setVisible(false);
1067            }  else if (e.getSource() == consistFunctionsPanelJIF) {
1068                throttleWindow.getViewConsistFunctionsPanel().setSelected(false);
1069                consistFunctionsPanelJIF.setVisible(false);
1070            }else {
1071                try { // #JYNSTRUMENT#, Very important, clean the Jynstrument
1072                    if ((e.getSource() instanceof JInternalFrame)) {
1073                        Component[] cmps = ((JInternalFrame) e.getSource()).getContentPane().getComponents();
1074                        int i = 0;
1075                        while ((i < cmps.length) && (!(cmps[i] instanceof Jynstrument))) {
1076                            i++;
1077                        }
1078                        if ((i < cmps.length) && (cmps[i] instanceof Jynstrument)) {
1079                            ((Jynstrument) cmps[i]).exit();
1080                        }
1081                    }
1082                } catch (Exception exc) {
1083                    log.debug("Got exception, can ignore: ", exc);
1084                }
1085            }
1086        }
1087
1088        /**
1089         * Listen for the activation of an internal frame record this property
1090         * for correct processing of the frame cycling key.
1091         *
1092         * @param e The InternalFrameEvent leading to this action
1093         */
1094        @Override
1095        public void internalFrameActivated(InternalFrameEvent e) {
1096            if (e.getSource() == controlPanelJIF) {
1097                activeFrame = CONTROL_PANEL_INDEX;
1098            } else if (e.getSource() == addressPanelJIF) {
1099                activeFrame = ADDRESS_PANEL_INDEX;
1100            } else if (e.getSource() == functionPanelJIF) {
1101                activeFrame = FUNCTION_PANEL_INDEX;
1102            } else if (e.getSource() == speedPanelJIF) {
1103                activeFrame = SPEED_DISPLAY_INDEX;
1104            } else if (e.getSource() == locoIconJIF) {
1105                activeFrame = LOCOICON_INDEX;
1106            } else if (e.getSource() == consistFunctionsPanelJIF) {
1107                activeFrame = CONSIST_FUNCTIONS_INDEX;
1108            }
1109        }
1110    }    
1111
1112    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ThrottleFrame.class);
1113}