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;
010import javax.swing.event.*;
011
012import jmri.DccThrottle;
013import jmri.InstanceManager;
014import jmri.LocoAddress;
015import jmri.Throttle;
016import jmri.jmrit.roster.Roster;
017import jmri.jmrit.roster.RosterEntry;
018import jmri.util.FileUtil;
019import jmri.util.gui.GuiLafPreferencesManager;
020import jmri.util.swing.OptionallyTabbedPanel;
021
022import org.jdom2.Element;
023
024/**
025 * A JInternalFrame that contains buttons for each decoder function.
026 */
027public class FunctionPanel extends JInternalFrame implements FunctionListener, java.beans.PropertyChangeListener, AddressListener {
028
029    private static final int DEFAULT_FUNCTION_BUTTONS = 24; // just enough to fill the initial pane
030    private static final int MAX_FUNCTION_BUTTONS_PER_TAB = 33; 
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 OptionallyTabbedPanel(MAX_FUNCTION_BUTTONS_PER_TAB);
206        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
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        scrollPane.getViewport().addChangeListener((e) -> viewPortSizeChanged(e));
216
217        setContentPane(scrollPane);
218        setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
219    }
220
221    private void viewPortSizeChanged(ChangeEvent e) {
222        // make sure function button area is laid out consistent with sizing
223        mainPanel.revalidate();
224    }
225    
226    private void setUpDefaultLightFunctionButton() {
227        try {
228            functionButtons[0].setIconPath("resources/icons/functionicons/svg/lightsOff.svg");
229            functionButtons[0].setSelectedIconPath("resources/icons/functionicons/svg/lightsOn.svg");
230        } catch (Exception e) {
231            log.debug("Exception loading svg icon : {}", e.getMessage());
232        } finally {
233            if ((functionButtons[0].getIcon() == null) || (functionButtons[0].getSelectedIcon() == null)) {
234                log.debug("Issue loading svg icon, reverting to png");
235                functionButtons[0].setIconPath("resources/icons/functionicons/transparent_background/lights_off.png");
236                functionButtons[0].setSelectedIconPath("resources/icons/functionicons/transparent_background/lights_on.png");
237            }
238        }
239    }
240
241    /**
242     * Apply preferences
243     *   + global throttles preferences
244     *   + this throttle settings if any
245     */
246    public final void applyPreferences() {
247        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
248        RosterEntry re = null;
249        if (mThrottle != null && addressPanel != null) {
250            re = addressPanel.getRosterEntry();
251        }
252        for (int i = 0; i < functionButtons.length; i++) {
253            if ((i == 0) && preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
254                setUpDefaultLightFunctionButton();
255            } else {
256                functionButtons[i].setIconPath(null);
257                functionButtons[i].setSelectedIconPath(null);
258            }
259            if (re != null) {
260                if (re.getFunctionLabel(i) != null) {
261                    functionButtons[i].setDisplay(re.getFunctionVisible(i));
262                    functionButtons[i].setButtonLabel(re.getFunctionLabel(i));
263                    if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
264                        functionButtons[i].setIconPath(re.getFunctionImage(i));
265                        functionButtons[i].setSelectedIconPath(re.getFunctionSelectedImage(i));
266                    } else {
267                        functionButtons[i].setIconPath(null);
268                        functionButtons[i].setSelectedIconPath(null);
269                    }
270                    functionButtons[i].setIsLockable(re.getFunctionLockable(i));
271                } else {
272                    functionButtons[i].setDisplay( ! (preferences.isUsingExThrottle() && preferences.isHidingUndefinedFuncButt()) );
273                }
274            }
275            functionButtons[i].updateLnF();
276        }
277    }
278
279    /**
280     * Rebuild function buttons
281     *
282     */
283    private void rebuildFnButons(int n) {
284        mainPanel.removeAll();
285        functionButtons = new FunctionButton[n];
286        for (int i = 0; i < functionButtons.length; i++) {
287            functionButtons[i] = new FunctionButton();
288            resetFnButton(functionButtons[i],i);
289            mainPanel.add(functionButtons[i]);
290            // Copy mouse and keyboard controls to new components
291            for (MouseWheelListener mwl:getMouseWheelListeners()) {
292                functionButtons[i].addMouseWheelListener(mwl);
293            }
294        }
295    }
296
297    /**
298     * Update function buttons
299     *    - from selected throttle setting and state
300     *    - from roster entry if any
301     */
302    private void updateFnButtons() {
303        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
304        if (mThrottle != null && addressPanel != null) {
305            RosterEntry rosterEntry = addressPanel.getRosterEntry();
306            if (rosterEntry != null) {
307                fnBtnUpdatedFromRoster = true;
308                log.debug("RosterEntry found: {}", rosterEntry.getId());
309            }
310            for (int i = 0; i < functionButtons.length; i++) {
311                // update from selected throttle setting
312                functionButtons[i].setEnabled(true);
313                functionButtons[i].setIdentity(i); // full reset of function
314                functionButtons[i].setThrottle(mThrottle);
315                functionButtons[i].setState(mThrottle.getFunction(i)); // reset button state
316                functionButtons[i].setIsLockable(!mThrottle.getFunctionMomentary(i));
317                functionButtons[i].setDropFolder(FileUtil.getUserResourcePath());
318                // update from roster entry if any
319                if (rosterEntry != null) {
320                    functionButtons[i].setDropFolder(Roster.getDefault().getRosterFilesLocation());
321                    boolean needUpdate = false;
322                    String imgButtonSize = rosterEntry.getAttribute("function"+i+"_ThrottleImageButtonSize");
323                    if (imgButtonSize != null) {
324                        try {
325                            functionButtons[i].setButtonImageSize(Integer.parseInt(imgButtonSize));
326                            needUpdate = true;
327                        } catch (NumberFormatException e) {
328                            log.debug("setFnButtons(): can't parse button image size attribute ");
329                        }
330                    }
331                    String text = rosterEntry.getFunctionLabel(i);
332                    if (text != null) {
333                        functionButtons[i].setDisplay(rosterEntry.getFunctionVisible(i));
334                        functionButtons[i].setButtonLabel(text);
335                        if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
336                            functionButtons[i].setIconPath(rosterEntry.getFunctionImage(i));
337                            functionButtons[i].setSelectedIconPath(rosterEntry.getFunctionSelectedImage(i));
338                        } else {
339                            functionButtons[i].setIconPath(null);
340                            functionButtons[i].setSelectedIconPath(null);
341                        }
342                        functionButtons[i].setIsLockable(rosterEntry.getFunctionLockable(i));
343                        needUpdate = true;
344                    } else if (preferences.isUsingExThrottle()
345                            && preferences.isHidingUndefinedFuncButt()) {
346                        functionButtons[i].setDisplay(false);
347                        needUpdate = true;
348                    }
349                    String fontSize = rosterEntry.getAttribute("function"+i+"_ThrottleFontSize");
350                    if (fontSize != null) {
351                        try {
352                            functionButtons[i].setFont(new Font("Monospaced", Font.PLAIN, Integer.parseInt(fontSize)));
353                            needUpdate = true;
354                        } catch (NumberFormatException e) {
355                            log.debug("setFnButtons(): can't parse font size attribute ");
356                        }
357                    }
358                    if (needUpdate) {
359                        functionButtons[i].updateLnF();
360                    }
361                }
362            }
363        }
364    }
365
366
367    private void resetFnButton(FunctionButton fb, int i) {
368        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
369        fb.setThrottle(mThrottle);
370        if (mThrottle!=null) {
371            fb.setState(mThrottle.getFunction(i)); // reset button state
372            fb.setIsLockable(!mThrottle.getFunctionMomentary(i));
373        }
374        fb.setIdentity(i);
375        fb.addFunctionListener(this);
376        fb.setButtonLabel( i<3 ? Bundle.getMessage(Throttle.getFunctionString(i)) : Throttle.getFunctionString(i) );
377        fb.setDisplay(true);
378        if ((i == 0) && preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
379            setUpDefaultLightFunctionButton();
380        } else {
381            fb.setIconPath(null);
382            fb.setSelectedIconPath(null);
383        }
384        fb.updateLnF();
385
386        // always display f0, F1 and F2
387        if (i < 3) {
388            fb.setVisible(true);
389        }
390    }
391
392    /**
393     * Reset function buttons :
394     *    - rebuild function buttons
395     *    - reset their properties to default
396     *    - update according to throttle and roster (if any)
397     *
398     */
399    public void resetFnButtons() {
400        // rebuild function buttons
401        if (mThrottle == null) {
402            rebuildFnButons(DEFAULT_FUNCTION_BUTTONS);
403        } else {
404            rebuildFnButons(mThrottle.getFunctions().length);
405        }
406        // reset their properties to defaults
407        for (int i = 0; i < functionButtons.length; i++) {
408            resetFnButton(functionButtons[i],i);
409        }
410        // update according to throttle and roster (if any)
411        updateFnButtons();
412        repaint();
413    }
414
415    /**
416     * Update the state of this panel if any of the functions change.
417     * {@inheritDoc}
418     */
419    @Override
420    public void propertyChange(java.beans.PropertyChangeEvent e) {
421        if (mThrottle!=null){
422            for (int i = 0; i < mThrottle.getFunctions().length; i++) {
423                if (e.getPropertyName().equals(Throttle.getFunctionString(i))) {
424                    setButtonByFuncNumber(i,false,(Boolean) e.getNewValue());
425                } else if (e.getPropertyName().equals(Throttle.getFunctionMomentaryString(i))) {
426                    setButtonByFuncNumber(i,true,!(Boolean) e.getNewValue());
427                }
428            }
429        }
430    }
431
432    private void setButtonByFuncNumber(int function, boolean lockable, boolean newVal){
433        for (FunctionButton button : functionButtons) {
434            if (button.getIdentity() == function) {
435                if (lockable) {
436                    button.setIsLockable(newVal);
437                } else {
438                    button.setState(newVal);
439                }
440            }
441        }
442    }
443
444    /**
445     * Collect the prefs of this object into XML Element.
446     * <ul>
447     * <li> Window prefs
448     * <li> Each button has id, text, lock state.
449     * </ul>
450     *
451     * @return the XML of this object.
452     */
453    public Element getXml() {
454        Element me = new Element("FunctionPanel"); // NOI18N
455        java.util.ArrayList<Element> children = new java.util.ArrayList<>(1 + functionButtons.length);
456        children.add(WindowPreferences.getPreferences(this));
457        for (FunctionButton functionButton : functionButtons) {
458            children.add(functionButton.getXml());
459        }
460        me.setContent(children);
461        return me;
462    }
463
464    /**
465     * Set the preferences based on the XML Element.
466     * <ul>
467     * <li> Window prefs
468     * <li> Each button has id, text, lock state.
469     * </ul>
470     *
471     * @param e The Element for this object.
472     */
473    public void setXml(Element e) {
474        Element window = e.getChild("window");
475        WindowPreferences.setPreferences(this, window);
476
477        if (! fnBtnUpdatedFromRoster) {
478            java.util.List<Element> buttonElements = e.getChildren("FunctionButton");
479
480            if (buttonElements != null && buttonElements.size() > 0) {
481                // just in case
482                rebuildFnButons( buttonElements.size() );
483                int i = 0;
484                for (Element buttonElement : buttonElements) {
485                    functionButtons[i++].setXml(buttonElement);
486                }
487            }
488        }
489    }
490
491    /**
492     * Get notification that a throttle has been found as we requested.
493     *
494     * @param t An instantiation of the DccThrottle with the address requested.
495     */
496    @Override
497    public void notifyAddressThrottleFound(DccThrottle t) {
498        log.debug("Throttle found for {}",t);
499        if (mThrottle != null) {
500            mThrottle.removePropertyChangeListener(this);
501        }
502        mThrottle = t;
503        mThrottle.addPropertyChangeListener(this);
504        int numFns = mThrottle.getFunctions().length;
505        if (addressPanel != null && addressPanel.getRosterEntry() != null) {
506            // +1 because we want the _number_ of functions, and we have to count F0
507            numFns = Math.min(numFns, addressPanel.getRosterEntry().getMaxFnNumAsInt()+1);
508        }
509        log.debug("notifyAddressThrottleFound number of functions {}", numFns);
510        resizeFnButtonsArray(numFns);
511        updateFnButtons();
512        setEnabled(true);
513    }
514
515    private void adressReleased() {
516        if (mThrottle != null) {
517            mThrottle.removePropertyChangeListener(this);
518        }
519        mThrottle = null;
520        fnBtnUpdatedFromRoster = false;
521        resetFnButtons();
522        setEnabled(false);
523    }
524
525    /**
526     * {@inheritDoc}
527     */
528    @Override
529    public void notifyAddressReleased(LocoAddress la) {
530        log.debug("Throttle released");
531        adressReleased();
532    }
533
534    /**
535     * Ignored.
536     * {@inheritDoc}
537     */
538    @Override
539    public void notifyAddressChosen(LocoAddress l) {
540    }
541
542    /**
543     * Ignored.
544     * {@inheritDoc}
545     */
546    @Override
547    public void notifyConsistAddressChosen(LocoAddress l) {
548    }
549
550    /**
551     * Ignored.
552     * {@inheritDoc}
553     */
554    @Override
555    public void notifyConsistAddressReleased(LocoAddress la) {
556        log.debug("Consist throttle released");
557        adressReleased();
558    }
559
560   /**
561     * Ignored.
562     * {@inheritDoc}
563     */
564    @Override
565    public void notifyConsistAddressThrottleFound(DccThrottle t) {
566        log.debug("Consist throttle found");
567        if (mThrottle == null) {
568            notifyAddressThrottleFound(t);
569        }
570    }
571
572    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FunctionPanel.class);
573}