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