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