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