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