001package jmri.jmrit.throttle.implementation;
002
003import java.io.File;
004import java.io.FileNotFoundException;
005import java.io.IOException;
006import java.util.ArrayList;
007import javax.swing.*;
008import jmri.DccLocoAddress;
009import jmri.DccThrottle;
010import jmri.InstanceManager;
011import jmri.LocoAddress;
012import jmri.ThrottleManager;
013import jmri.configurexml.StoreXmlConfigAction;
014import jmri.jmrit.XmlFile;
015import jmri.jmrit.roster.RosterEntry;
016import jmri.jmrit.throttle.ThrottleFrameManager;
017import jmri.jmrit.throttle.interfaces.AddressListener;
018import jmri.jmrit.throttle.interfaces.ThrottleControllerUI;
019import jmri.jmrit.throttle.panels.AddressPanel;
020import jmri.jmrit.throttle.panels.BackgroundPanel;
021import jmri.jmrit.throttle.panels.ConsistFunctionPanel;
022import jmri.jmrit.throttle.panels.ControlPanel;
023import jmri.jmrit.throttle.panels.FunctionPanel;
024import jmri.jmrit.throttle.panels.SpeedPanel;
025import jmri.jmrit.throttle.panels.LocoIconPanel;
026import jmri.jmrit.throttle.preferences.ThrottlesPreferences;
027import jmri.util.FileUtil;
028import jmri.util.swing.JmriJOptionPane;
029
030import org.jdom2.Document;
031import org.jdom2.Element;
032import org.jdom2.JDOMException;
033
034/**
035 * 
036 * Inner class of a throttle UI holding most of the logic.
037 * Used by classes actually implementing a throttle view (ThrottleFrame, SimpleThrottlePanel, ConsistFunctionPanel)
038 * 
039 * <hr>
040 * This file is part of JMRI.
041 * <p>
042 * JMRI is free software; you can redistribute it and/or modify it under the
043 * terms of version 2 of the GNU General Public License as published by the Free
044 * Software Foundation. See the "COPYING" file for a copy of this license.
045 * <p>
046 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
047 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
048 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
049 *
050 * @author Glen Oberhauser
051 * @author Andrew Berridge Copyright 2010
052 * @author Lionel Jeanson 2026
053 * 
054 */
055
056public class ThrottleUICore implements AddressListener  {
057
058    private DccThrottle throttle;
059    private final ThrottleManager throttleManager;
060    private final ThrottleFrameManager throttleFrameManager = InstanceManager.getDefault(ThrottleFrameManager.class);
061    private final ThrottleControllerUI myThrottleController;
062    private final boolean withPopupMenu; // should panel show the contextual menu that enable customization
063
064    private ControlPanel controlPanel;
065    private FunctionPanel functionPanel;
066    private AddressPanel addressPanel;
067    private BackgroundPanel backgroundPanel;
068    private SpeedPanel speedPanel;
069    private LocoIconPanel locoIconPanel;
070    private ConsistFunctionPanel consistFunctionsPanel;
071
072    private String lastUsedSaveFile = null;
073
074    private static final String DEFAULT_THROTTLE_FILENAME = "JMRI_ThrottlePreference.xml";
075
076    public static String getDefaultThrottleFolder() {
077        return FileUtil.getUserFilesPath() + "throttle" + File.separator;
078    }
079
080    public static String getDefaultThrottleFilename() {
081        return getDefaultThrottleFolder() + DEFAULT_THROTTLE_FILENAME;
082    }
083
084    public ThrottleUICore(ThrottleManager tm, ThrottleControllerUI tc, boolean withPopupMenu) {
085        super();
086        throttleManager = tm;
087        myThrottleController = tc;
088        this.withPopupMenu = withPopupMenu;
089        initGUI();
090    }
091
092    public ThrottleUICore(ThrottleManager tm, ThrottleControllerUI tc) {
093        this(tm,tc,true);
094    }
095
096    public AddressPanel getAddressPanel() {
097        return addressPanel;
098    }
099
100    public DccThrottle getThrottle() {
101        return getAddressPanel().getThrottle();  
102    }
103
104    public DccThrottle getFunctionThrottle() {
105        if (getAddressPanel().getConsistAddress() == null) {
106            return getThrottle();
107        }
108        return getConsistFunctionsPanel().getFunctionThrottle();        
109    }    
110
111    public RosterEntry getRosterEntry() {
112        return addressPanel.getRosterEntry();
113    }
114
115    public RosterEntry getFunctionRosterEntry() {
116        if (getAddressPanel().getConsistAddress() == null) {
117            return getRosterEntry();
118        }        
119        return getConsistFunctionsPanel().getFunctionRosterEntry();                
120    }    
121
122    public ControlPanel getControlPanel() {
123        if (controlPanel == null) { // init only when requested
124            controlPanel = new ControlPanel(throttleManager, withPopupMenu);
125            controlPanel.setAddressPanel(addressPanel);
126        }        
127        return controlPanel;
128    }
129
130    public FunctionPanel getFunctionPanel() {
131        if (functionPanel == null) { // init only when requested
132            functionPanel = new FunctionPanel(withPopupMenu);
133            functionPanel.setAddressPanel(addressPanel);
134        }          
135        return functionPanel;
136    }
137
138    public SpeedPanel getSpeedPanel() {
139        if (speedPanel == null) { // init only when requested
140            speedPanel = new SpeedPanel();
141            speedPanel.setAddressPanel(addressPanel);
142        }
143        return speedPanel;
144    }
145
146    public ConsistFunctionPanel getConsistFunctionsPanel() {
147        if (consistFunctionsPanel == null) { // init only when requested
148            consistFunctionsPanel = new ConsistFunctionPanel(throttleManager);
149            consistFunctionsPanel.setAddressPanel(addressPanel);
150        }
151        return consistFunctionsPanel;
152    }
153
154    public BackgroundPanel getBackgroundPanel() {
155        if (backgroundPanel == null) { // init only when requested
156            backgroundPanel = new BackgroundPanel();
157            backgroundPanel.setAddressPanel(addressPanel);
158        }
159        return backgroundPanel;
160    }
161
162    public LocoIconPanel getLocoIconPanel() {
163        if (locoIconPanel == null) { // init only when requested
164            locoIconPanel = new LocoIconPanel();
165            locoIconPanel.setAddressPanel(addressPanel);
166        }        
167        return locoIconPanel;
168    }
169
170    public boolean hasActiveFunction() {
171        if (getAddressPanel().getThrottle() == null) {
172            return false;
173        }
174        for (boolean b : getAddressPanel().getThrottle().getFunctions() ) {
175            if (b) {
176                return true;
177            }
178        }
179        return false;
180    }   
181
182    private void initGUI() {
183        // create panels that are actually required for a throttle
184        // some (speed Panel, backgroundPanel) will be created on demand only (see getters)
185        addressPanel = new AddressPanel(throttleManager);            
186        addressPanel.setEnabled(true);
187        addressPanel.addAddressListener(this);                       
188    }
189
190    public void setRosterEntry(RosterEntry re) {
191        getAddressPanel().setRosterEntry(re);
192    }
193    
194    public void setAddress(DccLocoAddress la) {
195        getAddressPanel().setCurrentAddress(la);
196    }
197
198    public DccLocoAddress getAddress() {
199        return getAddressPanel().getCurrentAddress();
200    }
201
202    public void setConsistAddress(DccLocoAddress la) {
203        getAddressPanel().setConsistAddress(la);
204    }
205        
206    public void eStop() {
207        DccThrottle throt = getAddressPanel().getThrottle();
208        if (throt != null) {
209            throt.setSpeedSetting(-1);
210        }
211    }
212
213    /**
214     * Handle my own destruction.
215     * <ol>
216     * <li> dispose of sub windows.
217     * <li> notify my manager of my demise.
218     * </ol>
219     */
220    public void dispose() {
221        log.debug("Disposing");
222        addressPanel.removeAddressListener(this);
223        // should the throttle list table stop listening to that throttle?
224        if (throttle!=null &&  throttleFrameManager.getNumberOfEntriesFor((DccLocoAddress) throttle.getLocoAddress()) == 0 ) { // 0 because this throtte frame window has been removed from the list already, so this is last chance to remove listener
225            throttleManager.removeListener(throttle.getLocoAddress(), throttleFrameManager.getThrottlesListPanel().getTableModel());
226            throttleFrameManager.getThrottlesListPanel().getTableModel().fireTableDataChanged();
227        }
228        // check for any special disposing in InternalFrames
229        if (controlPanel!=null) {
230            controlPanel.dispose();
231        }
232        if (functionPanel!=null) {
233            functionPanel.dispose();
234        }
235        if (speedPanel!=null) {
236            speedPanel.dispose();
237        }
238        if (backgroundPanel!=null) {
239            backgroundPanel.dispose();      
240        }
241        if (consistFunctionsPanel!=null) {
242            consistFunctionsPanel.dispose();
243        }
244        // dispose of this last because it will release and destroy the throttle.
245        addressPanel.dispose();
246    }
247
248    public void saveRosterChanges() {
249        RosterEntry rosterEntry = addressPanel.getRosterEntry();
250        if (rosterEntry == null) {
251            JmriJOptionPane.showMessageDialog(this.getAddressPanel(), Bundle.getMessage("ThrottleFrameNoRosterItemMessageDialog"),
252                Bundle.getMessage("ThrottleFrameNoRosterItemTitleDialog"), JmriJOptionPane.ERROR_MESSAGE);
253            return;
254        }
255        if ((!InstanceManager.getDefault(ThrottlesPreferences.class).isSavingThrottleOnLayoutSave()) && (JmriJOptionPane.showConfirmDialog(this.getAddressPanel(), Bundle.getMessage("ThrottleFrameRosterChangeMesageDialog"),
256            Bundle.getMessage("ThrottleFrameRosterChangeTitleDialog"), JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION)) {
257            return;
258        }
259        if (functionPanel!=null) {
260            functionPanel.saveFunctionButtonsToRoster(rosterEntry);
261        }
262        if (controlPanel!=null) {
263            controlPanel.saveToRoster(rosterEntry);
264        }
265    }
266
267    /**
268     * Collect the prefs of this object into the given XML Element array
269     * 
270     * @param children the array to fill with the XML Elements for this object.  The caller will add these to the parent element as needed.
271     * 
272     */
273    public void getXml(ArrayList<Element> children) {
274        if (controlPanel != null) {
275            children.add(controlPanel.getXml());
276        }
277        if (functionPanel != null) {
278            children.add(functionPanel.getXml());
279        }
280        children.add(addressPanel.getXml());
281        if (speedPanel != null) { 
282            children.add(speedPanel.getXml());
283        }
284        if (locoIconPanel != null) {
285            children.add(locoIconPanel.getXml());
286        }
287        if (consistFunctionsPanel != null) {
288            children.add(consistFunctionsPanel.getXml());
289        }
290    }
291
292    /**
293     * Set the preferences based on the XML Element.
294     * <ul>
295     * <li> Window prefs
296     * <li> Frame title
297     * <li> ControlPanel
298     * <li> FunctionPanel
299     * <li> AddressPanel
300     * <li> SpeedPanel
301     * </ul>
302     *
303     * @param e The Element for this object.
304     */
305    public void setXml(Element e) {
306        if (e == null) {
307            return;
308        }
309        Element child = e.getChild("AddressPanel");
310        addressPanel.setXml(child);
311        child = e.getChild("ControlPanel");
312        if (child != null) {
313            getControlPanel().setXml(child);
314        }
315        child = e.getChild("FunctionPanel");
316        if (child != null) {
317            getFunctionPanel().setXml(child);
318        }
319        child = e.getChild("SpeedPanel");
320        if (child != null) {
321            getSpeedPanel().setXml(child);
322        }
323        child = e.getChild("LocoIconPanel");
324        if (child != null) {
325            getLocoIconPanel().setXml(child);
326        }
327        child = e.getChild("ConsistFunctionsPanel");
328        if (child != null &&
329            //  SimpleThrottlePanel being used to implement ConsistFunctionsPanel, avoid loading recursively
330            //  when using CS consist, xml for consist will be head unit one, that will be reloaded again and agin here
331          (myThrottleController != null) && (myThrottleController.getThrottleControllersContainer() != null)) {
332            getConsistFunctionsPanel().setXml(child);
333        }
334    }
335
336    public void saveThrottleAs(Element throttleElement) {
337        JFileChooser fileChooser = jmri.jmrit.XmlFile.userFileChooser(Bundle.getMessage("PromptXmlFileTypes"), "xml");
338        fileChooser.setCurrentDirectory(new File(getDefaultThrottleFolder()));
339        fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
340        java.io.File file = StoreXmlConfigAction.getFileName(fileChooser);
341        if (file == null) {
342            return;
343        }
344        saveThrottle(throttleElement, file.getAbsolutePath());
345    }
346
347    public void saveThrottle(Element throttleElement) {
348        if (getRosterEntry() != null) {
349            saveThrottle(throttleElement, ThrottleUICore.getDefaultThrottleFolder() + getRosterEntry().getId().trim() + ".xml");
350        } else if (getLastUsedSaveFile() != null) {
351            saveThrottle(throttleElement, getLastUsedSaveFile());
352        }
353    }
354
355    private void saveThrottle(Element throttleElement, String sfile) {
356        // Save throttle: title / window position
357        // as strongly linked to extended throttles and roster presence, do not save function buttons and background window as they're stored in the roster entry
358        XmlFile xf = new XmlFile() {
359        };   // odd syntax is due to XmlFile being abstract
360        xf.makeBackupFile(sfile);
361        File file = new File(sfile);
362        try {
363            //The file does not exist, create it before writing
364            File parentDir = file.getParentFile();
365            if (!parentDir.exists()) {
366                if (!parentDir.mkdir()) { // make directory and check result
367                    log.error("could not make parent directory");
368                }
369            }
370            if (!file.createNewFile()) { // create file, check success
371                log.error("createNewFile failed");
372            }
373        } catch (IOException exp) {
374            log.error("Exception while writing the throttle file, may not be complete: {}", exp.getMessage());
375        }
376
377        try {
378            Element root = new Element("throttle-config");
379            root.setAttribute("noNamespaceSchemaLocation",  // NOI18N
380                    "http://jmri.org/xml/schema/throttle-config.xsd",  // NOI18N
381                    org.jdom2.Namespace.getNamespace("xsi",
382                            "http://www.w3.org/2001/XMLSchema-instance"));  // NOI18N
383            Document doc = new Document(root);
384
385            // add XSLT processing instruction
386            // <?xml-stylesheet type="text/xsl" href="XSLT/throttle.xsl"?>
387            java.util.Map<String,String> m = new java.util.HashMap<String, String>();
388            m.put("type", "text/xsl");
389            m.put("href", jmri.jmrit.XmlFile.xsltLocation + "throttle-config.xsl");
390            org.jdom2.ProcessingInstruction p = new org.jdom2.ProcessingInstruction("xml-stylesheet", m);
391            doc.addContent(0,p);
392            
393            // don't save the loco address or consist address
394            //   throttleElement.getChild("AddressPanel").removeChild("locoaddress");
395            //   throttleElement.getChild("AddressPanel").removeChild("locoaddress");
396            if ((getRosterEntry() != null) &&
397                    (ThrottleUICore.getDefaultThrottleFolder() + getRosterEntry().getId().trim() + ".xml").compareTo(sfile) == 0) // don't save function buttons labels, they're in roster entry
398            {
399                throttleElement.getChild("FunctionPanel").removeChildren("FunctionButton");
400                saveRosterChanges();
401            } 
402
403            root.setContent(throttleElement);
404            xf.writeXML(file, doc);
405            setLastUsedSaveFile(sfile);
406        } catch (IOException ex) {
407            log.warn("Exception while storing throttle xml: {}", ex.getMessage());
408        }
409    }
410
411    public Element loadThrottle(String sfile) throws IOException, NullPointerException, JDOMException, FileNotFoundException {
412        log.debug("Loading throttle file : {}", sfile);
413        if (sfile == null) {
414            return null;
415        }
416        
417        XmlFile xf = new XmlFile() {};   // odd syntax is due to XmlFile being abstract
418        xf.setValidate(XmlFile.Validate.CheckDtdThenSchema);
419        File f = new File(sfile);
420        Element root = xf.rootFromFile(f);
421        Element conf = root.getChild("ThrottleFrame");
422        // File looks ok
423        setLastUsedSaveFile(sfile);
424        // and finally load all preferences
425        setXml(conf);
426        // and return it to be used by caller if needed
427        return conf;
428    }
429
430    private boolean isLoadingDefault = false;
431
432    public void loadDefaultThrottle() {
433        if (isLoadingDefault) { // avoid looping on this method
434            return; 
435        }
436        isLoadingDefault = true;
437        String dtf = InstanceManager.getDefault(ThrottlesPreferences.class).getDefaultThrottleFilePath();
438        if (dtf == null || dtf.isEmpty()) {
439            return;
440        }
441        log.debug("Loading default throttle file : {}", dtf);
442        if (myThrottleController!=null) {
443            myThrottleController.loadThrottleFile(dtf);
444        }
445        setLastUsedSaveFile(null);
446        isLoadingDefault = false;
447    }
448
449    @Override
450    public void notifyAddressChosen(LocoAddress l) {
451    }
452
453    @Override
454    public void notifyRosterEntrySelected(RosterEntry re) {     
455    }
456
457    @Override
458    public void notifyAddressReleased(LocoAddress la) {
459        if (throttle == null) {
460            log.debug("notifyAddressReleased() throttle already null, called for loc {}",la);
461            return;
462        }        
463        if (throttleFrameManager.getNumberOfEntriesFor((DccLocoAddress) throttle.getLocoAddress()) == 1 )  {
464            throttleManager.removeListener(throttle.getLocoAddress(), throttleFrameManager.getThrottlesListPanel().getTableModel());
465        }        throttle = null;
466        setLastUsedSaveFile(null);
467        if (myThrottleController!=null) {
468            myThrottleController.updateFrameTitle();
469            myThrottleController.updateGUI();
470        }
471        throttleFrameManager.getThrottlesListPanel().getTableModel().fireTableStructureChanged();        
472    }
473
474    @Override
475    public void notifyAddressThrottleFound(DccThrottle t) {
476        if (throttle != null) {
477            log.debug("notifyAddressThrottleFound() throttle non null, called for loc {}",t.getLocoAddress());
478            return;
479        }
480        throttle = t;
481        if ((InstanceManager.getDefault(ThrottlesPreferences.class).isUsingExThrottle())
482                && (InstanceManager.getDefault(ThrottlesPreferences.class).isAutoLoading()) && (addressPanel != null)) {
483            if ((addressPanel.getRosterEntry() != null)
484                    && ((getLastUsedSaveFile() == null) || (getLastUsedSaveFile().compareTo(getDefaultThrottleFolder() + addressPanel.getRosterEntry().getId().trim() + ".xml") != 0))) {
485                if (myThrottleController!=null) {
486                    myThrottleController.loadThrottleFile(getDefaultThrottleFolder() + addressPanel.getRosterEntry().getId().trim() + ".xml");
487                }
488                setLastUsedSaveFile(getDefaultThrottleFolder() + addressPanel.getRosterEntry().getId().trim() + ".xml");
489            } else if ((addressPanel.getRosterEntry() == null)
490                    && ((getLastUsedSaveFile() == null) || (getLastUsedSaveFile().compareTo(getDefaultThrottleFolder() + addressPanel.getCurrentAddress()+ ".xml") != 0))) {
491                if (myThrottleController!=null) {
492                    myThrottleController.loadThrottleFile(getDefaultThrottleFolder() + throttle.getLocoAddress().getNumber() + ".xml");
493                }
494                setLastUsedSaveFile(getDefaultThrottleFolder() + throttle.getLocoAddress().getNumber() + ".xml");
495            }
496        } else {
497            if ((addressPanel != null) && (addressPanel.getRosterEntry() == null)) { // no known roster entry
498                loadDefaultThrottle();
499            }
500        }
501        if (myThrottleController!=null) {
502            myThrottleController.updateFrameTitle();
503            myThrottleController.updateGUI();
504        }
505        throttleFrameManager.getThrottlesListPanel().getTableModel().fireTableDataChanged();
506    }
507    
508    @Override
509    public void notifyConsistAddressChosen(LocoAddress l) {
510        notifyAddressChosen(l);
511    }
512    
513    @Override
514    public void notifyConsistAddressReleased(LocoAddress la) {
515        notifyAddressReleased(la);
516    }
517
518    @Override
519    public void notifyConsistAddressThrottleFound(DccThrottle throttle) {
520        notifyAddressThrottleFound(throttle);
521    }
522
523    public String getLastUsedSaveFile() {
524        return lastUsedSaveFile;
525    }
526
527    public void setLastUsedSaveFile(String lusf) {
528        lastUsedSaveFile = lusf;
529        if (myThrottleController!=null) {
530            myThrottleController.updateGUI();
531        }
532    }
533
534    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ThrottleUICore.class);
535}