001package jmri.jmrit.throttle.panels;
002
003import java.awt.*;
004import java.awt.event.*;
005import java.beans.PropertyChangeListener;
006import java.util.Arrays;
007
008import javax.swing.*;
009import javax.swing.border.Border;
010import javax.swing.border.EmptyBorder;
011
012import jmri.DccThrottle;
013import jmri.InstanceManager;
014import jmri.LocoAddress;
015import jmri.Throttle;
016import jmri.jmrit.roster.Roster;
017import jmri.jmrit.roster.RosterEntry;
018import jmri.jmrit.roster.swing.functiontable.FunctionTableModel;
019import jmri.jmrit.throttle.interfaces.AddressListener;
020import jmri.jmrit.throttle.interfaces.FunctionListener;
021import jmri.jmrit.throttle.preferences.ThrottlesPreferences;
022import jmri.util.FileUtil;
023import jmri.util.gui.GuiLafPreferencesManager;
024import jmri.util.swing.OptionallyTabbedPanel;
025
026import org.jdom2.Element;
027
028import com.fasterxml.jackson.core.JsonProcessingException;
029import com.fasterxml.jackson.databind.ObjectMapper;
030
031/**
032 * A Panel that contains buttons for each decoder function.
033 * 
034 * <hr>
035 * This file is part of JMRI.
036 * <p>
037 * JMRI is free software; you can redistribute it and/or modify it under the
038 * terms of version 2 of the GNU General Public License as published by the Free
039 * Software Foundation. See the "COPYING" file for a copy of this license.
040 * <p>
041 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
042 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
043 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
044 *
045 */
046public class FunctionPanel extends OptionallyTabbedPanel implements FunctionListener, PropertyChangeListener, AddressListener {
047
048    private static final int DEFAULT_FUNCTION_BUTTONS = 24; // just enough to fill the initial pane
049    private static final int MAX_FUNCTION_BUTTONS_PER_TAB = 33; 
050    private DccThrottle mThrottle;
051
052    private FunctionButton[] functionButtons;
053    private int[] btnIdxFn; // fn button index to fn map (see function buttons display order)
054
055    private boolean withPopupMenuOnFnButtons;
056    private boolean fnBtnUpdatedFromRoster = false; // avoid to reinit function button twice (from throttle xml and from roster)
057
058    private AddressPanel addressPanel = null; // to access roster infos
059
060    /**
061     * Constructor
062     * 
063     *  @param withPopupMenu  popup menu on function button available if true
064     * 
065     */
066    public FunctionPanel(boolean withPopupMenu) {
067        super(MAX_FUNCTION_BUTTONS_PER_TAB);
068        InstanceManager.getDefault(ThrottlesPreferences.class).addPropertyChangeListener(this);
069        withPopupMenuOnFnButtons = withPopupMenu;
070        initGUI();        
071    }
072
073    public FunctionPanel() {
074        this(true);
075    }
076
077    public void dispose() {
078        InstanceManager.getDefault(ThrottlesPreferences.class).removePropertyChangeListener(this);
079        if (functionButtons != null) {
080            for (FunctionButton fb : functionButtons) {
081                fb.destroy();
082                fb.removeFunctionListener(this);
083            }
084            functionButtons = null;
085        }
086        if (addressPanel != null) {
087            addressPanel.removeAddressListener(this);
088            addressPanel = null;
089        }
090        if (mThrottle != null) {
091            mThrottle.removePropertyChangeListener(this);
092            mThrottle = null;
093        }
094    }
095
096    public FunctionButton[] getFunctionButtons() {
097        return Arrays.copyOf(functionButtons, functionButtons.length);
098    }
099
100    /**
101     * Get notification that a function has changed state.
102     *
103     * @param functionNumber The function that has changed.
104     * @param isSet          True if the function is now active (or set).
105     */
106    @Override
107    public void notifyFunctionStateChanged(int functionNumber, boolean isSet) {
108        log.debug("notifyFunctionStateChanged: fNumber={} isSet={} " ,functionNumber, isSet);
109        if (mThrottle != null) {
110            log.debug("setting throttle {} function {}", mThrottle.getLocoAddress(), functionNumber);
111            mThrottle.setFunction(functionNumber, isSet);
112        }
113    }
114
115    /**
116     * Get notification that a function's lockable status has changed.
117     *
118     * @param functionNumber The function that has changed (0-28).
119     * @param isLockable     True if the function is now Lockable (continuously
120     *                       active).
121     */
122    @Override
123    public void notifyFunctionLockableChanged(int functionNumber, boolean isLockable) {
124        log.debug("notifyFnLockableChanged: fNumber={} isLockable={} " ,functionNumber, isLockable);
125        if (mThrottle != null) {
126            log.debug("setting throttle {} function momentary {}", mThrottle.getLocoAddress(), functionNumber);
127            mThrottle.setFunctionMomentary(functionNumber, !isLockable);
128        }
129    }
130
131    /**
132     * Enable or disable all the buttons.
133     * @param isEnabled true to enable, false to disable.
134     */
135    @Override
136    public void setEnabled(boolean isEnabled) {
137        for (FunctionButton functionButton : functionButtons) {
138            functionButton.setEnabled(isEnabled);
139        }
140    }
141
142    /**
143     * Enable or disable all the buttons depending on throttle status
144     * If a throttle is assigned, enable all, else disable all
145     */
146    public void setEnabled() {
147        setEnabled(mThrottle != null);
148    }
149
150    public void setAddressPanel(AddressPanel ap) {
151        if (addressPanel != null) {
152            addressPanel.removeAddressListener(this);
153        }
154        addressPanel = ap;
155        if (addressPanel != null) {
156            addressPanel.addAddressListener(this);        
157            if (addressPanel.getThrottle() != null ) {
158                notifyAddressThrottleFound(addressPanel.getThrottle());
159            } else {
160                notifyAddressReleased(addressPanel.getCurrentAddress());
161            }
162        }
163    }
164
165    public void saveFunctionButtonsToRoster(RosterEntry rosterEntry) {
166        log.debug("saveFunctionButtonsToRoster");
167        if (rosterEntry == null) {
168            return;
169        }
170        for (FunctionButton functionButton : functionButtons) {
171            int functionNumber = functionButton.getIdentity();
172            String text = functionButton.getButtonLabel();
173            boolean lockable = functionButton.getIsLockable();
174            boolean visible = functionButton.getDisplay();
175            String imagePath = functionButton.getIconPath();
176            String imageSelectedPath = functionButton.getSelectedIconPath();
177            if (functionButton.isDirty()) {
178                if (!text.equals(rosterEntry.getFunctionLabel(functionNumber))) {
179                    if (text.isEmpty()) {
180                        text = null;  // reset button text to default
181                    }
182                    rosterEntry.setFunctionLabel(functionNumber, text);
183                }
184                String fontSizeKey = "function"+functionNumber+"_ThrottleFontSize";
185                if (rosterEntry.getAttribute(fontSizeKey) != null && functionButton.getFont().getSize() == InstanceManager.getDefault(GuiLafPreferencesManager.class).getFontSize()) {
186                    rosterEntry.deleteAttribute(fontSizeKey);
187                }
188                if (functionButton.getFont().getSize() != InstanceManager.getDefault(GuiLafPreferencesManager.class).getFontSize()) {
189                    rosterEntry.putAttribute(fontSizeKey, ""+functionButton.getFont().getSize());
190                }
191                String imgButtonSizeKey = "function"+functionNumber+"_ThrottleImageButtonSize";
192                if (rosterEntry.getAttribute(imgButtonSizeKey) != null && functionButton.getButtonImageSize() == FunctionButton.DEFAULT_IMG_SIZE) {
193                    rosterEntry.deleteAttribute(imgButtonSizeKey);
194                }
195                if (functionButton.getButtonImageSize() != FunctionButton.DEFAULT_IMG_SIZE) {
196                    rosterEntry.putAttribute(imgButtonSizeKey, ""+functionButton.getButtonImageSize());
197                }
198                if (rosterEntry.getFunctionLabel(functionNumber) != null ) {
199                    if( lockable != rosterEntry.getFunctionLockable(functionNumber)) {
200                        rosterEntry.setFunctionLockable(functionNumber, lockable);
201                    }
202                    if( visible != rosterEntry.getFunctionVisible(functionNumber)) {
203                        rosterEntry.setFunctionVisible(functionNumber, visible);
204                    }
205                    if ( (!imagePath.isEmpty() && rosterEntry.getFunctionImage(functionNumber) == null )
206                            || (rosterEntry.getFunctionImage(functionNumber) != null && imagePath.compareTo(rosterEntry.getFunctionImage(functionNumber)) != 0)) {
207                        rosterEntry.setFunctionImage(functionNumber, imagePath);
208                    }
209                    if ( (!imageSelectedPath.isEmpty() && rosterEntry.getFunctionSelectedImage(functionNumber) == null )
210                            || (rosterEntry.getFunctionSelectedImage(functionNumber) != null && imageSelectedPath.compareTo(rosterEntry.getFunctionSelectedImage(functionNumber)) != 0)) {
211                        rosterEntry.setFunctionSelectedImage(functionNumber, imageSelectedPath);
212                    }
213                }
214                functionButton.setDirty(false);
215            }
216        }
217        Roster.getDefault().writeRoster();
218    }
219
220    /**
221     * Place and initialize all the buttons.
222     */
223    private void initGUI() {        
224        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
225        // init function buttons
226        resizeFnButons(DEFAULT_FUNCTION_BUTTONS);
227        // update according to throttle and roster (if any)
228        applyFnButtonsLnF();
229        JScrollPane scrollPane = new JScrollPane(this);
230        scrollPane.getViewport().setOpaque(false); // container already gets this done (for play/edit mode)
231        scrollPane.setOpaque(false);
232        setOpaque(false);
233        Border empyBorder = new EmptyBorder(0,0,0,0); // force look'n feel, no border
234        scrollPane.setViewportBorder( empyBorder );
235        scrollPane.setBorder( empyBorder );
236        scrollPane.setWheelScrollingEnabled(false); // already used by speed slider
237        scrollPane.getViewport().addChangeListener((e) -> revalidate());
238    }
239
240    /**
241     * Resize or build inner function button array
242     *
243     */
244    private void resizeFnButons(int numFns) {
245        // Resize inner function buttons array
246        FunctionButton[] newFunctionButtons = new FunctionButton[numFns];
247        int prevl = 0;
248        if (functionButtons != null) {
249            System.arraycopy(functionButtons, 0, newFunctionButtons, 0, Math.min( functionButtons.length, numFns));
250            prevl = functionButtons.length ;
251        }
252        if (numFns > prevl) {
253            for (int i=prevl;i<numFns;i++) {
254                newFunctionButtons[i] = new FunctionButton(withPopupMenuOnFnButtons);
255                add(newFunctionButtons[i]);
256                resetFnButton(newFunctionButtons[i],i);
257                // Copy mouse and keyboard controls to new components
258                for (MouseWheelListener mwl:getMouseWheelListeners()) {
259                   newFunctionButtons[i].addMouseWheelListener(mwl);
260                }
261            }
262        }
263        functionButtons = newFunctionButtons;
264    }
265
266    /**
267     * Update function buttons look'n feel
268     *    - from default layout setting if any
269     *    - from selected throttle setting and state
270     *    - from roster entry if any
271     */
272    public void applyFnButtonsLnF() {
273        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
274        final RosterEntry rosterEntry = (addressPanel != null?addressPanel.getRosterEntry():null);
275        btnIdxFn = null;
276        // 1 - reset to default xml layout if any
277        setXml(preferences.getFunctionPanelDefaultXML()); 
278        // retrieve function buttons order from roster entry if any
279        if (rosterEntry != null) { 
280            fnBtnUpdatedFromRoster = true;
281            log.debug("RosterEntry found: {}", rosterEntry.getId());
282            String fnDOAtt = rosterEntry.getAttribute(FunctionTableModel.FB_ORDER_ATTR);
283            btnIdxFn = null;
284            if (fnDOAtt != null) {
285                try {
286                    btnIdxFn = new ObjectMapper().readValue(fnDOAtt, int[].class );
287                } catch (JsonProcessingException e) {
288                    log.warn("Couldn't parse FnDisplayOrder attribute ",e);
289                } 
290            }    
291        }
292        for (int i = 0; i < functionButtons.length; i++) {
293            if (preferences.getFunctionPanelDefaultXML() == null) { // we don't have a default xml layout, hard reset each button
294                resetFnButton(functionButtons[i],i );
295            }
296            // update from selected throttle setting
297            functionButtons[i].setEnabled(true);
298            functionButtons[i].setIdentity(i); // full reset of function
299            functionButtons[i].setDropFolder(FileUtil.getUserResourcePath());
300            functionButtons[i].setThrottle(mThrottle);
301            if (mThrottle != null) {
302                functionButtons[i].setState(mThrottle.getFunction(i)); // reset button state
303                if (preferences.getFunctionPanelDefaultXML() == null) { // we don't have a default layout, ask the throttle 
304                    functionButtons[i].setIsLockable(!mThrottle.getFunctionMomentary(i));
305                }
306            }                
307            // update from roster entry if any
308            if (rosterEntry != null) {
309                int fn = i;
310                if ((btnIdxFn != null) && (i < btnIdxFn.length)) {
311                    fn = btnIdxFn[i];
312                }    
313                functionButtons[i].setIdentity(fn);                
314                functionButtons[i].setDropFolder(Roster.getDefault().getRosterFilesLocation());
315                String imgButtonSize = rosterEntry.getAttribute("function"+fn+"_ThrottleImageButtonSize");
316                if (imgButtonSize != null) {
317                    try {
318                        functionButtons[i].setButtonImageSize(Integer.parseInt(imgButtonSize));
319                    } catch (NumberFormatException e) {
320                        log.debug("setFnButtons(): can't parse button image size attribute ");
321                    }
322                }
323                functionButtons[i].setButtonLabel( i<3 ? Bundle.getMessage(Throttle.getFunctionString(i)) : Throttle.getFunctionString(i) );
324                String text = rosterEntry.getFunctionLabel(fn);
325                if (text != null) {
326                    functionButtons[i].setDisplay(rosterEntry.getFunctionVisible(fn));
327                    functionButtons[i].setButtonLabel(text);
328                    if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
329                        functionButtons[i].setIconPath(rosterEntry.getFunctionImage(fn));
330                        functionButtons[i].setSelectedIconPath(rosterEntry.getFunctionSelectedImage(fn));
331                    } else {
332                        functionButtons[i].setIconPath(null);
333                        functionButtons[i].setSelectedIconPath(null);
334                    }
335                    functionButtons[i].setIsLockable(rosterEntry.getFunctionLockable(fn));
336                } else if (preferences.isUsingExThrottle()
337                        && preferences.isHidingUndefinedFuncButt()) {
338                    functionButtons[i].setDisplay(false);
339                } else {
340                    functionButtons[i].setDisplay(true);
341                    functionButtons[i].setButtonLabel( fn<3 ? Bundle.getMessage(Throttle.getFunctionString(fn)) : Throttle.getFunctionString(fn) );
342                }
343                String fontSize = rosterEntry.getAttribute("function"+fn+"_ThrottleFontSize");
344                if (fontSize != null) {
345                    try {
346                        functionButtons[i].setFont(new Font("Monospaced", Font.PLAIN, Integer.parseInt(fontSize)));
347                    } catch (NumberFormatException e) {
348                        log.debug("setFnButtons(): can't parse font size attribute ");
349                    }
350                }
351                functionButtons[i].updateLnF();                
352            }
353        }        
354    }
355
356    private void resetFnButton(FunctionButton fb, int i) {
357        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
358        fb.setThrottle(mThrottle);
359        if (mThrottle!=null) {
360            fb.setState(mThrottle.getFunction(i)); // reset button state
361            fb.setIsLockable(!mThrottle.getFunctionMomentary(i));
362        }
363        fb.setIdentity(i);
364        fb.addFunctionListener(this);
365        fb.setButtonLabel( i<3 ? Bundle.getMessage(Throttle.getFunctionString(i)) : Throttle.getFunctionString(i) );
366        fb.setDisplay(true);
367        if ((i == 0) && preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
368            try {
369                fb.setIconPath("resources/icons/functionicons/svg/lightsOff.svg");
370                fb.setSelectedIconPath("resources/icons/functionicons/svg/lightsOn.svg");
371            } catch (Exception e) {
372                log.debug("Exception loading svg icon : {}", e.getMessage());
373            } finally {
374                if ((fb.getIcon() == null) || (fb.getSelectedIcon() == null)) {
375                    log.debug("Issue loading svg icon, reverting to png");
376                    fb.setIconPath("resources/icons/functionicons/transparent_background/lights_off.png");
377                    fb.setSelectedIconPath("resources/icons/functionicons/transparent_background/lights_on.png");
378                }
379            } 
380        } else {
381            fb.setIconPath(null);
382            fb.setSelectedIconPath(null);
383        }
384        fb.updateLnF();
385
386        // always display f0, F1 and F2
387        if (i < 3) {
388            fb.setVisible(true);
389        }
390    }
391
392    /**
393     * Update the state of this panel if any of the functions change.
394     * {@inheritDoc}
395     */
396    @Override
397    public void propertyChange(java.beans.PropertyChangeEvent e) {
398        if (e == null) {
399            return;
400        }
401        log.debug("Property change event received {} / {}", e.getPropertyName(), e.getNewValue());        
402        if (mThrottle!=null){
403            for (int i = 0; i < mThrottle.getFunctions().length; i++) {
404                if (e.getPropertyName().equals(Throttle.getFunctionString(i))) {
405                    setButtonByFuncNumber(i,false,(Boolean) e.getNewValue());
406                } else if (e.getPropertyName().equals(Throttle.getFunctionMomentaryString(i))) {
407                    setButtonByFuncNumber(i,true,!(Boolean) e.getNewValue());
408                }
409            }
410        }
411        if (ThrottlesPreferences.prefPopertyName.compareTo(e.getPropertyName()) == 0) {
412            applyFnButtonsLnF();
413            setEnabled();
414        }        
415    }
416
417    private void setButtonByFuncNumber(int function, boolean lockable, boolean newVal){
418        for (FunctionButton button : functionButtons) {
419            if (button.getIdentity() == function) {
420                if (lockable) {
421                    button.setIsLockable(newVal);
422                } else {
423                    button.setState(newVal);
424                }
425            }
426        }
427    }
428
429    /**
430     * Collect the prefs of this object into XML Element.
431     * <ul>
432     * <li> Window prefs
433     * <li> Each button has id, text, lock state.
434     * </ul>
435     *
436     * @return the XML of this object.
437     */
438    public Element getXml() {
439        Element me = new Element("FunctionPanel"); // NOI18N
440        java.util.ArrayList<Element> children = new java.util.ArrayList<>(1 + functionButtons.length);
441        for (FunctionButton functionButton : functionButtons) {
442            children.add(functionButton.getXml());
443        }
444        me.setContent(children);
445        return me;
446    }
447
448    /**
449     * Set the preferences based on the XML Element.
450     * <ul>
451     * <li> Window prefs
452     * <li> Each button has id, text, lock state.
453     * </ul>
454     *
455     * @param e The Element for this object.
456     */
457    public void setXml(Element e) {
458        if (e == null) {
459            return;
460        }  
461        if (! fnBtnUpdatedFromRoster) {
462            java.util.List<Element> buttonElements = e.getChildren("FunctionButton");
463
464            if (buttonElements != null && buttonElements.size() > 0) {
465                // just in case
466                resizeFnButons( buttonElements.size() );
467                int i = 0;
468                for (Element buttonElement : buttonElements) {
469                    functionButtons[i++].setXml(buttonElement);
470                }                
471            }
472            setEnabled(); // refresh state
473        }
474    }
475
476    @Override
477    public void notifyAddressThrottleFound(DccThrottle t) {
478        log.debug("Throttle found for {}",t);
479        if (mThrottle != null) {
480            mThrottle.removePropertyChangeListener(this);
481        }
482        mThrottle = t;
483        mThrottle.addPropertyChangeListener(this);
484        if (addressPanel!=null) {
485            notifyRosterEntryUpdated(addressPanel.getRosterEntry() );
486        } else {
487            notifyRosterEntryUpdated(null);
488        }
489        setEnabled(true);
490    }
491
492    private void adressReleased() {
493        if (mThrottle != null) {
494            mThrottle.removePropertyChangeListener(this);
495        }
496        mThrottle = null;
497        fnBtnUpdatedFromRoster = false;
498        setEnabled(false);
499    }
500
501    @Override
502    public void notifyAddressReleased(LocoAddress la) {
503        log.debug("Throttle released");
504        adressReleased();
505    }
506
507    @Override
508    public void notifyAddressChosen(LocoAddress l) {
509    }
510
511    @Override
512    public void notifyNewRosterEntryHighlighted(RosterEntry re) {     
513    }
514
515    @Override
516    public void notifyRosterEntryUpdated(RosterEntry re) {
517        int numFns = mThrottle.getFunctions().length;
518        if (re != null) {
519            // +1 because we want the _number_ of functions, and we have to count F0
520            numFns = Math.min(numFns, re.getMaxFnNumAsInt()+1);
521        }
522        log.debug("notifyRosterEntryUpdated number of functions {}", numFns);
523        resizeFnButons(numFns);
524        applyFnButtonsLnF();
525    }
526
527    @Override
528    public void notifyConsistAddressChosen(LocoAddress l) {
529    }
530
531    @Override
532    public void notifyConsistAddressReleased(LocoAddress la) {
533        log.debug("Consist throttle released");
534        adressReleased();
535    }
536
537    @Override
538    public void notifyConsistAddressThrottleFound(DccThrottle t) {
539        log.debug("Consist throttle found");
540        if (mThrottle == null) {
541            notifyAddressThrottleFound(t);
542        }
543    }
544
545    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FunctionPanel.class);
546}