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            boolean visible = functionButton.getDisplay();
156            String imagePath = functionButton.getIconPath();
157            String imageSelectedPath = functionButton.getSelectedIconPath();
158            if (functionButton.isDirty()) {
159                if (!text.equals(rosterEntry.getFunctionLabel(functionNumber))) {
160                    if (text.isEmpty()) {
161                        text = null;  // reset button text to default
162                    }
163                    rosterEntry.setFunctionLabel(functionNumber, text);
164                }
165                String fontSizeKey = "function"+functionNumber+"_ThrottleFontSize";
166                if (rosterEntry.getAttribute(fontSizeKey) != null && functionButton.getFont().getSize() == InstanceManager.getDefault(GuiLafPreferencesManager.class).getFontSize()) {
167                    rosterEntry.deleteAttribute(fontSizeKey);
168                }
169                if (functionButton.getFont().getSize() != InstanceManager.getDefault(GuiLafPreferencesManager.class).getFontSize()) {
170                    rosterEntry.putAttribute(fontSizeKey, ""+functionButton.getFont().getSize());
171                }
172                String imgButtonSizeKey = "function"+functionNumber+"_ThrottleImageButtonSize";
173                if (rosterEntry.getAttribute(imgButtonSizeKey) != null && functionButton.getButtonImageSize() == FunctionButton.DEFAULT_IMG_SIZE) {
174                    rosterEntry.deleteAttribute(imgButtonSizeKey);
175                }
176                if (functionButton.getButtonImageSize() != FunctionButton.DEFAULT_IMG_SIZE) {
177                    rosterEntry.putAttribute(imgButtonSizeKey, ""+functionButton.getButtonImageSize());
178                }
179                if (rosterEntry.getFunctionLabel(functionNumber) != null ) {
180                    if( lockable != rosterEntry.getFunctionLockable(functionNumber)) {
181                        rosterEntry.setFunctionLockable(functionNumber, lockable);
182                    }
183                    if( visible != rosterEntry.getFunctionVisible(functionNumber)) {
184                        rosterEntry.setFunctionVisible(functionNumber, visible);
185                    }
186                    if ( (!imagePath.isEmpty() && rosterEntry.getFunctionImage(functionNumber) == null )
187                            || (rosterEntry.getFunctionImage(functionNumber) != null && imagePath.compareTo(rosterEntry.getFunctionImage(functionNumber)) != 0)) {
188                        rosterEntry.setFunctionImage(functionNumber, imagePath);
189                    }
190                    if ( (!imageSelectedPath.isEmpty() && rosterEntry.getFunctionSelectedImage(functionNumber) == null )
191                            || (rosterEntry.getFunctionSelectedImage(functionNumber) != null && imageSelectedPath.compareTo(rosterEntry.getFunctionSelectedImage(functionNumber)) != 0)) {
192                        rosterEntry.setFunctionSelectedImage(functionNumber, imageSelectedPath);
193                    }
194                }
195                functionButton.setDirty(false);
196            }
197        }
198        Roster.getDefault().writeRoster();
199    }
200
201    /**
202     * Place and initialize all the buttons.
203     */
204    private void initGUI() {
205        mainPanel = new JPanel();
206        mainPanel.setLayout(new WrapLayout(FlowLayout.CENTER, 2, 2));
207        resetFnButtons();
208        JScrollPane scrollPane = new JScrollPane(mainPanel);
209        scrollPane.getViewport().setOpaque(false); // container already gets this done (for play/edit mode)
210        scrollPane.setOpaque(false);
211        Border empyBorder = new EmptyBorder(0,0,0,0); // force look'n feel, no border
212        scrollPane.setViewportBorder( empyBorder );
213        scrollPane.setBorder( empyBorder );
214        scrollPane.setWheelScrollingEnabled(false); // already used by speed slider
215        setContentPane(scrollPane);
216        setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
217    }
218
219    private void setUpDefaultLightFunctionButton() {
220        try {
221            functionButtons[0].setIconPath("resources/icons/functionicons/svg/lightsOff.svg");
222            functionButtons[0].setSelectedIconPath("resources/icons/functionicons/svg/lightsOn.svg");
223        } catch (Exception e) {
224            log.debug("Exception loading svg icon : {}", e.getMessage());
225        } finally {
226            if ((functionButtons[0].getIcon() == null) || (functionButtons[0].getSelectedIcon() == null)) {
227                log.debug("Issue loading svg icon, reverting to png");
228                functionButtons[0].setIconPath("resources/icons/functionicons/transparent_background/lights_off.png");
229                functionButtons[0].setSelectedIconPath("resources/icons/functionicons/transparent_background/lights_on.png");
230            }
231        }
232    }
233
234    /**
235     * Apply preferences
236     *   + global throttles preferences
237     *   + this throttle settings if any
238     */
239    public final void applyPreferences() {
240        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
241        RosterEntry re = null;
242        if (mThrottle != null && addressPanel != null) {
243            re = addressPanel.getRosterEntry();
244        }
245        for (int i = 0; i < functionButtons.length; i++) {
246            if ((i == 0) && preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
247                setUpDefaultLightFunctionButton();
248            } else {
249                functionButtons[i].setIconPath(null);
250                functionButtons[i].setSelectedIconPath(null);
251            }
252            if (re != null) {
253                if (re.getFunctionLabel(i) != null) {
254                    functionButtons[i].setDisplay(re.getFunctionVisible(i));
255                    functionButtons[i].setButtonLabel(re.getFunctionLabel(i));
256                    if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
257                        functionButtons[i].setIconPath(re.getFunctionImage(i));
258                        functionButtons[i].setSelectedIconPath(re.getFunctionSelectedImage(i));
259                    } else {
260                        functionButtons[i].setIconPath(null);
261                        functionButtons[i].setSelectedIconPath(null);
262                    }
263                    functionButtons[i].setIsLockable(re.getFunctionLockable(i));
264                } else {
265                    functionButtons[i].setDisplay( ! (preferences.isUsingExThrottle() && preferences.isHidingUndefinedFuncButt()) );
266                }
267            }
268            functionButtons[i].updateLnF();
269        }
270    }
271
272    /**
273     * Rebuild function buttons
274     *
275     */
276    private void rebuildFnButons(int n) {
277        mainPanel.removeAll();
278        functionButtons = new FunctionButton[n];
279        for (int i = 0; i < functionButtons.length; i++) {
280            functionButtons[i] = new FunctionButton();
281            resetFnButton(functionButtons[i],i);
282            mainPanel.add(functionButtons[i]);
283            // Copy mouse and keyboard controls to new components
284            for (MouseWheelListener mwl:getMouseWheelListeners()) {
285                functionButtons[i].addMouseWheelListener(mwl);
286            }
287        }
288    }
289
290    /**
291     * Update function buttons
292     *    - from selected throttle setting and state
293     *    - from roster entry if any
294     */
295    private void updateFnButtons() {
296        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
297        if (mThrottle != null && addressPanel != null) {
298            RosterEntry rosterEntry = addressPanel.getRosterEntry();
299            if (rosterEntry != null) {
300                fnBtnUpdatedFromRoster = true;
301                log.debug("RosterEntry found: {}", rosterEntry.getId());
302            }
303            for (int i = 0; i < functionButtons.length; i++) {
304                // update from selected throttle setting
305                functionButtons[i].setEnabled(true);
306                functionButtons[i].setIdentity(i); // full reset of function
307                functionButtons[i].setThrottle(mThrottle);
308                functionButtons[i].setState(mThrottle.getFunction(i)); // reset button state
309                functionButtons[i].setIsLockable(!mThrottle.getFunctionMomentary(i));
310                functionButtons[i].setDropFolder(FileUtil.getUserResourcePath());
311                // update from roster entry if any
312                if (rosterEntry != null) {
313                    functionButtons[i].setDropFolder(Roster.getDefault().getRosterFilesLocation());
314                    boolean needUpdate = false;
315                    String imgButtonSize = rosterEntry.getAttribute("function"+i+"_ThrottleImageButtonSize");
316                    if (imgButtonSize != null) {
317                        try {
318                            functionButtons[i].setButtonImageSize(Integer.parseInt(imgButtonSize));
319                            needUpdate = true;
320                        } catch (NumberFormatException e) {
321                            log.debug("setFnButtons(): can't parse button image size attribute ");
322                        }
323                    }
324                    String text = rosterEntry.getFunctionLabel(i);
325                    if (text != null) {
326                        functionButtons[i].setDisplay(rosterEntry.getFunctionVisible(i));
327                        functionButtons[i].setButtonLabel(text);
328                        if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
329                            functionButtons[i].setIconPath(rosterEntry.getFunctionImage(i));
330                            functionButtons[i].setSelectedIconPath(rosterEntry.getFunctionSelectedImage(i));
331                        } else {
332                            functionButtons[i].setIconPath(null);
333                            functionButtons[i].setSelectedIconPath(null);
334                        }
335                        functionButtons[i].setIsLockable(rosterEntry.getFunctionLockable(i));
336                        needUpdate = true;
337                    } else if (preferences.isUsingExThrottle()
338                            && preferences.isHidingUndefinedFuncButt()) {
339                        functionButtons[i].setDisplay(false);
340                        needUpdate = true;
341                    }
342                    String fontSize = rosterEntry.getAttribute("function"+i+"_ThrottleFontSize");
343                    if (fontSize != null) {
344                        try {
345                            functionButtons[i].setFont(new Font("Monospaced", Font.PLAIN, Integer.parseInt(fontSize)));
346                            needUpdate = true;
347                        } catch (NumberFormatException e) {
348                            log.debug("setFnButtons(): can't parse font size attribute ");
349                        }
350                    }
351                    if (needUpdate) {
352                        functionButtons[i].updateLnF();
353                    }
354                }
355            }
356        }
357    }
358
359
360    private void resetFnButton(FunctionButton fb, int i) {
361        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
362        fb.setThrottle(mThrottle);
363        if (mThrottle!=null) {
364            fb.setState(mThrottle.getFunction(i)); // reset button state
365            fb.setIsLockable(!mThrottle.getFunctionMomentary(i));
366        }
367        fb.setIdentity(i);
368        fb.addFunctionListener(this);
369        fb.setButtonLabel( i<3 ? Bundle.getMessage(Throttle.getFunctionString(i)) : Throttle.getFunctionString(i) );
370        fb.setDisplay(true);
371        if ((i == 0) && preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
372            setUpDefaultLightFunctionButton();
373        } else {
374            fb.setIconPath(null);
375            fb.setSelectedIconPath(null);
376        }
377        fb.updateLnF();
378
379        // always display f0, F1 and F2
380        if (i < 3) {
381            fb.setVisible(true);
382        }
383    }
384
385    /**
386     * Reset function buttons :
387     *    - rebuild function buttons
388     *    - reset their properties to default
389     *    - update according to throttle and roster (if any)
390     *
391     */
392    public void resetFnButtons() {
393        // rebuild function buttons
394        if (mThrottle == null) {
395            rebuildFnButons(DEFAULT_FUNCTION_BUTTONS);
396        } else {
397            rebuildFnButons(mThrottle.getFunctions().length);
398        }
399        // reset their properties to defaults
400        for (int i = 0; i < functionButtons.length; i++) {
401            resetFnButton(functionButtons[i],i);
402        }
403        // update according to throttle and roster (if any)
404        updateFnButtons();
405        repaint();
406    }
407
408    /**
409     * Update the state of this panel if any of the functions change.
410     * {@inheritDoc}
411     */
412    @Override
413    public void propertyChange(java.beans.PropertyChangeEvent e) {
414        if (mThrottle!=null){
415            for (int i = 0; i < mThrottle.getFunctions().length; i++) {
416                if (e.getPropertyName().equals(Throttle.getFunctionString(i))) {
417                    setButtonByFuncNumber(i,false,(Boolean) e.getNewValue());
418                } else if (e.getPropertyName().equals(Throttle.getFunctionMomentaryString(i))) {
419                    setButtonByFuncNumber(i,true,!(Boolean) e.getNewValue());
420                }
421            }
422        }
423    }
424
425    private void setButtonByFuncNumber(int function, boolean lockable, boolean newVal){
426        for (FunctionButton button : functionButtons) {
427            if (button.getIdentity() == function) {
428                if (lockable) {
429                    button.setIsLockable(newVal);
430                } else {
431                    button.setState(newVal);
432                }
433            }
434        }
435    }
436
437    /**
438     * Collect the prefs of this object into XML Element.
439     * <ul>
440     * <li> Window prefs
441     * <li> Each button has id, text, lock state.
442     * </ul>
443     *
444     * @return the XML of this object.
445     */
446    public Element getXml() {
447        Element me = new Element("FunctionPanel"); // NOI18N
448        java.util.ArrayList<Element> children = new java.util.ArrayList<>(1 + functionButtons.length);
449        children.add(WindowPreferences.getPreferences(this));
450        for (FunctionButton functionButton : functionButtons) {
451            children.add(functionButton.getXml());
452        }
453        me.setContent(children);
454        return me;
455    }
456
457    /**
458     * Set the preferences based on the XML Element.
459     * <ul>
460     * <li> Window prefs
461     * <li> Each button has id, text, lock state.
462     * </ul>
463     *
464     * @param e The Element for this object.
465     */
466    public void setXml(Element e) {
467        Element window = e.getChild("window");
468        WindowPreferences.setPreferences(this, window);
469
470        if (! fnBtnUpdatedFromRoster) {
471            java.util.List<Element> buttonElements = e.getChildren("FunctionButton");
472
473            if (buttonElements != null && buttonElements.size() > 0) {
474                // just in case
475                rebuildFnButons( buttonElements.size() );
476                int i = 0;
477                for (Element buttonElement : buttonElements) {
478                    functionButtons[i++].setXml(buttonElement);
479                }
480            }
481        }
482    }
483
484    /**
485     * Get notification that a throttle has been found as we requested.
486     *
487     * @param t An instantiation of the DccThrottle with the address requested.
488     */
489    @Override
490    public void notifyAddressThrottleFound(DccThrottle t) {
491        log.debug("Throttle found for {}",t);
492        if (mThrottle != null) {
493            mThrottle.removePropertyChangeListener(this);
494        }
495        mThrottle = t;
496        mThrottle.addPropertyChangeListener(this);
497        int numFns = mThrottle.getFunctions().length;
498        if (addressPanel != null && addressPanel.getRosterEntry() != null) {
499            // +1 because we want the _number_ of functions, and we have to count F0
500            numFns = Math.min(numFns, addressPanel.getRosterEntry().getMaxFnNumAsInt()+1);
501        }
502        log.debug("notifyAddressThrottleFound number of functions {}", numFns);
503        resizeFnButtonsArray(numFns);
504        updateFnButtons();
505        setEnabled(true);
506    }
507
508    private void adressReleased() {
509        if (mThrottle != null) {
510            mThrottle.removePropertyChangeListener(this);
511        }
512        mThrottle = null;
513        fnBtnUpdatedFromRoster = false;
514        resetFnButtons();
515        setEnabled(false);
516    }
517
518    /**
519     * {@inheritDoc}
520     */
521    @Override
522    public void notifyAddressReleased(LocoAddress la) {
523        log.debug("Throttle released");
524        adressReleased();
525    }
526
527    /**
528     * Ignored.
529     * {@inheritDoc}
530     */
531    @Override
532    public void notifyAddressChosen(LocoAddress l) {
533    }
534
535    /**
536     * Ignored.
537     * {@inheritDoc}
538     */
539    @Override
540    public void notifyConsistAddressChosen(LocoAddress l) {
541    }
542
543    /**
544     * Ignored.
545     * {@inheritDoc}
546     */
547    @Override
548    public void notifyConsistAddressReleased(LocoAddress la) {
549        log.debug("Consist throttle released");
550        adressReleased();
551    }
552
553   /**
554     * Ignored.
555     * {@inheritDoc}
556     */
557    @Override
558    public void notifyConsistAddressThrottleFound(DccThrottle t) {
559        log.debug("Consist throttle found");
560        if (mThrottle == null) {
561            notifyAddressThrottleFound(t);
562        }
563    }
564
565    private final static Logger log = LoggerFactory.getLogger(FunctionPanel.class);
566}