001package jmri.jmrit.throttle.panels;
002
003import com.fasterxml.jackson.core.JsonProcessingException;
004import com.fasterxml.jackson.databind.ObjectMapper;
005
006import java.awt.*;
007import java.awt.event.*;
008import java.awt.image.BufferedImage;
009import java.beans.PropertyChangeEvent;
010import java.beans.PropertyChangeListener;
011import java.io.IOException;
012import java.util.*;
013
014import javax.swing.*;
015import javax.swing.event.ChangeEvent;
016import javax.swing.plaf.basic.BasicSliderUI;
017
018import jmri.*;
019import jmri.jmrit.roster.Roster;
020import jmri.jmrit.roster.RosterEntry;
021import jmri.jmrit.throttle.ThrottleFrameManager;
022import jmri.jmrit.throttle.interfaces.AddressListener;
023import jmri.jmrit.throttle.preferences.ThrottlesPreferences;
024import jmri.util.FileUtil;
025import jmri.util.MouseInputAdapterInstaller;
026import jmri.util.swing.JmriMouseAdapter;
027import jmri.util.swing.JmriMouseEvent;
028import jmri.util.swing.JmriMouseListener;
029
030import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
031import org.apache.batik.transcoder.*;
032import org.apache.batik.transcoder.image.ImageTranscoder;
033import org.apache.batik.util.XMLResourceDescriptor;
034import org.jdom2.Element;
035import org.jdom2.Attribute;
036import org.w3c.dom.Document;
037
038/**
039 * A JPanel that contains a JSlider to control loco speed, and buttons
040 * for forward, reverse and STOP.
041 * 
042 * <hr>
043 * This file is part of JMRI.
044 * <p>
045 * JMRI is free software; you can redistribute it and/or modify it under the
046 * terms of version 2 of the GNU General Public License as published by the Free
047 * Software Foundation. See the "COPYING" file for a copy of this license.
048 * <p>
049 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
050 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
051 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
052 *
053 * @author glen Copyright (C) 2002
054 * @author Bob Jacobsen Copyright (C) 2007, 2021
055 * @author Ken Cameron Copyright (C) 2008
056 * @author Lionel Jeanson 2009-2026
057 */
058public class ControlPanel extends JPanel implements PropertyChangeListener, AddressListener {
059
060    private final ThrottleManager throttleManager;
061    private final ThrottleFrameManager throttleFrameManager = InstanceManager.getDefault(ThrottleFrameManager.class);
062
063    private DccThrottle throttle;
064    private boolean isConsist = false;
065
066    private JSlider speedSlider;
067    private JSlider speedSliderContinuous;
068    private JSpinner speedSpinner;
069    private SpinnerNumberModel speedSpinnerModel;
070    private JComboBox<SpeedStepMode> speedStepBox;
071    private JRadioButton forwardButton, reverseButton;
072    private JButton stopButton;
073    private JButton idleButton;
074    private JPanel buttonPanel;
075    private JPanel topButtonPanel;
076
077    private Document forwardButtonSvgIcon;
078    private Document forwardSelectedButtonSvgIcon;
079    private Document forwardRollButtonSvgIcon;
080    private ImageIcon forwardButtonImageIcon;
081    private ImageIcon forwardSelectedButtonImageIcon;
082    private ImageIcon forwardRollButtonImageIcon;
083
084    private Document reverseButtonSvgIcon;
085    private Document reverseSelectedButtonSvgIcon;
086    private Document reverseRollButtonSvgIcon;
087    private ImageIcon reverseButtonImageIcon;
088    private ImageIcon reverseSelectedButtonImageIcon;
089    private ImageIcon reverseRollButtonImageIcon;
090
091    private Document idleButtonSvgIcon;
092    private Document idleSelectedButtonSvgIcon;
093    private Document idleRollButtonSvgIcon;
094    private ImageIcon idleButtonImageIcon;
095    private ImageIcon idleSelectedButtonImageIcon;
096    private ImageIcon idleRollButtonImageIcon;
097
098    private Document stopButtonSvgIcon;
099    private Document stopSelectedButtonSvgIcon;
100    private Document stopRollButtonSvgIcon;
101    private ImageIcon stopButtonImageIcon;
102    private ImageIcon stopSelectedButtonImageIcon;
103    private ImageIcon stopRollButtonImageIcon;
104    
105    private ImageIcon speedLabelVerticalImageIcon;
106    private ImageIcon speedLabelHorizontalImageIcon;
107    
108    private Map<Integer, JLabel> defaultLabelTable;    
109    private Map<Integer, JLabel> verticalLabelMap;
110    private Map<Integer, JLabel> horizontalLabelMap;
111
112    private boolean internalAdjust = false; // protecting the speed slider, continuous slider and spinner when doing internal adjust
113
114    private JPopupMenu popupMenu;
115    private ControlPanelPropertyEditor propertyEditor;
116    private JPanel speedControlPanel;
117    private JPanel spinnerPanel;
118    private JPanel sliderPanel;
119    private JPanel speedSliderContinuousPanel;
120
121    private AddressPanel addressPanel; //for access to roster entry
122    /* Constants for speed selection method */
123    public static final int SLIDERDISPLAY = 0;
124    public static final int STEPDISPLAY = 1;
125    public static final int SLIDERDISPLAYCONTINUOUS = 2;
126
127    public static final int DEFAULT_BUTTON_SIZE = 24;
128    private static final String LONGEST_SS_STRING="999";
129    private static final int FONT_SIZE_MIN=12;
130    private static final int FONT_INCREMENT = 2;
131
132    private int _displaySlider = SLIDERDISPLAY;
133
134    /* real time tracking of speed slider - on iff trackSlider==true
135     * Min interval for sending commands to the actual throttle can be configured
136     * as part of the throttle config but is bounded
137     */
138
139    private boolean trackSlider = false;
140    private boolean hideSpeedStep = false;
141    private final boolean trackSliderDefault = false;
142    private long trackSliderMinInterval = 200;         // milliseconds
143    private final long trackSliderMinIntervalDefault = 200;  // milliseconds
144    private final long trackSliderMinIntervalMin = 50;       // milliseconds
145    private final long trackSliderMinIntervalMax = 1000;     // milliseconds
146    private long lastTrackedSliderMovementTime = 0;
147
148    // LocoNet really only has 126 speed steps i.e. 0..127 - 1 for em stop
149    private int intSpeedSteps = 126;
150
151    private int maxSpeed = 126; //The maximum permissible speed
152
153    private boolean speedControllerEnable = false;
154
155    // Switch to continuous slider on function...
156    private String switchSliderFunction = "Fxx";
157    private String prevShuntingFn = null;
158
159    /**
160     * Constructor.
161     */
162    public ControlPanel() {
163        this(InstanceManager.getDefault(ThrottleManager.class), true);
164    }
165
166    /**
167     * Constructor.
168     * @param tm the throttle manager
169     * @param withPopupMenu popup menu on control panel available if true
170     */
171    public ControlPanel(ThrottleManager tm, boolean withPopupMenu) {
172        throttleManager = tm;
173        initGUI(withPopupMenu);
174        InstanceManager.getDefault(ThrottlesPreferences.class).addPropertyChangeListener(this);
175        applyPreferences();
176    }
177
178    /*
179     * Set the AddressPanel this throttle control is listenning for new throttle event
180     */
181    public void setAddressPanel(AddressPanel ap) {
182        if (addressPanel != null) {
183            addressPanel.removeAddressListener(this);
184        }
185        addressPanel = ap;
186        if (addressPanel != null) {
187            addressPanel.addAddressListener(this);     
188            if (addressPanel.getThrottle() != null ) {
189                notifyAddressThrottleFound(addressPanel.getThrottle());
190            } else {
191                notifyAddressReleased(addressPanel.getCurrentAddress());
192            }
193        }
194    }
195
196    public void dispose() {
197        InstanceManager.getDefault(ThrottlesPreferences.class).removePropertyChangeListener(this);
198        if (addressPanel != null) {
199            addressPanel.removeAddressListener(this);
200            addressPanel = null;
201        }
202        if (throttle != null) {
203            throttle.removePropertyChangeListener(this);
204            throttle = null;
205        }
206    }
207
208    /**
209     * Enable/Disable all buttons and slider.
210     *
211     * @param isEnabled True if the buttons/slider should be enabled, false
212     *                  otherwise.
213     */
214    @Override
215    public void setEnabled(boolean isEnabled) {
216        forwardButton.setEnabled(isEnabled);
217        reverseButton.setEnabled(isEnabled);
218        speedStepBox.setEnabled(isEnabled);
219        stopButton.setEnabled(isEnabled);
220        idleButton.setEnabled(isEnabled);
221        speedControllerEnable = isEnabled;
222        switch (_displaySlider) {
223            case STEPDISPLAY: {
224                speedSpinner.setEnabled(isEnabled);
225                speedSliderContinuous.setEnabled(false);                
226                speedSlider.setEnabled(false);
227                break;
228            }
229            case SLIDERDISPLAYCONTINUOUS: {
230                speedSliderContinuous.setEnabled(isEnabled);            
231                speedSpinner.setEnabled(false);                
232                speedSlider.setEnabled(false);
233                break;
234            }
235            default: {
236                speedSpinner.setEnabled(false);
237                speedSliderContinuous.setEnabled(false);
238                speedSlider.setEnabled(isEnabled);
239            }
240        }
241    }
242
243    /**
244     * is this enabled?
245     * @return true if enabled
246     */
247    @Override
248    public boolean isEnabled() {
249        return speedControllerEnable;
250    }
251
252    /**
253     * Set the GUI to match that the loco is set to forward.
254     *
255     * @param isForward True if the loco is set to forward, false otherwise.
256     */
257    private void setIsForward(boolean isForward) {
258        forwardButton.setSelected(isForward);
259        reverseButton.setSelected(!isForward);
260        internalAdjust = true;
261        if (isForward) {
262            speedSliderContinuous.setValue(java.lang.Math.abs(speedSliderContinuous.getValue()));
263        } else {
264            speedSliderContinuous.setValue(-java.lang.Math.abs(speedSliderContinuous.getValue()));
265        }
266        internalAdjust = false;        
267    }
268
269    /**
270     * Set the GUI to match the speed steps of the current address. Initialises
271     * the speed slider and spinner - including setting their maximums based on
272     * the speed step setting and the max speed for the particular loco
273     *
274     * @param speedStepMode Desired speed step mode. One of:
275     *                      SpeedStepMode.NMRA_DCC_128,
276     *                      SpeedStepMode.NMRA_DCC_28,
277     *                      SpeedStepMode.NMRA_DCC_27,
278     *                      SpeedStepMode.NMRA_DCC_14 step mode
279     */
280    public void setSpeedStepsMode(SpeedStepMode speedStepMode) {
281        internalAdjust = true;
282        int maxSpeedPCT = 100;
283        if (addressPanel != null && addressPanel.getRosterEntry() != null) {
284            maxSpeedPCT = addressPanel.getRosterEntry().getMaxSpeedPCT();
285        }
286
287        // Save the old speed as a float
288        float oldSpeed = (speedSlider.getValue() / (maxSpeed * 1.0f));
289
290        if (speedStepMode == SpeedStepMode.UNKNOWN) {
291            speedStepMode = (SpeedStepMode) speedStepBox.getSelectedItem();
292        } else {
293            speedStepBox.setSelectedItem(speedStepMode);
294        }
295        intSpeedSteps = speedStepMode.numSteps;
296
297        /* Set maximum speed based on the max speed stored in the roster as a percentage of the maximum */
298        maxSpeed = (int) ((float) intSpeedSteps * ((float) maxSpeedPCT) / 100);
299
300        // rescale the speed slider to match the new speed step mode
301        speedSlider.setMaximum(maxSpeed);
302        speedSlider.setValue((int) (oldSpeed * maxSpeed));
303        speedSlider.setMajorTickSpacing(maxSpeed / 2);
304
305        speedSliderContinuous.setMaximum(maxSpeed);
306        speedSliderContinuous.setMinimum(-maxSpeed);
307        if (forwardButton.isSelected()) {
308            speedSliderContinuous.setValue((int) (oldSpeed * maxSpeed));
309        } else {
310            speedSliderContinuous.setValue(-(int) (oldSpeed * maxSpeed));
311        }
312        speedSliderContinuous.setMajorTickSpacing(maxSpeed / 2);
313
314        computeLabelsTable();
315        updateSlidersLabelDisplay();
316                
317        speedSpinnerModel.setMaximum(maxSpeed);
318        speedSpinnerModel.setMinimum(0);
319        // rescale the speed value to match the new speed step mode
320        speedSpinnerModel.setValue(speedSlider.getValue());
321        internalAdjust = false;
322    }
323
324    /**
325     * Is this Speed Control selection method possible?
326     *
327     * @param displaySlider integer value. possible values: SLIDERDISPLAY = use
328     *                      speed slider display STEPDISPLAY = use speed step
329     *                      display
330     * @return true if speed controller of the selected type is available.
331     */
332    public boolean isSpeedControllerAvailable(int displaySlider) {
333        switch (displaySlider) {
334            case STEPDISPLAY:
335            case SLIDERDISPLAY:
336            case SLIDERDISPLAYCONTINUOUS:
337                return true;
338            default:
339                return false;
340        }
341    }
342
343    /**
344     * Set the Speed Control selection method
345     *
346     * @param displaySlider integer value. possible values: SLIDERDISPLAY = use
347     *                      speed slider display STEPDISPLAY = use speed step
348     *                      display
349     */
350    public void setSpeedController(int displaySlider) {
351        _displaySlider = displaySlider;
352        switch (displaySlider) {
353            case STEPDISPLAY:
354                sliderPanel.setVisible(false);
355                speedSlider.setEnabled(false);
356                speedSliderContinuousPanel.setVisible(false);
357                speedSliderContinuous.setEnabled(false);                
358                spinnerPanel.setVisible(true);
359                speedSpinner.setEnabled(speedControllerEnable);
360                return;
361                
362            case SLIDERDISPLAYCONTINUOUS:
363                sliderPanel.setVisible(false);
364                speedSlider.setEnabled(false);
365                speedSliderContinuousPanel.setVisible(true);
366                speedSliderContinuous.setEnabled(speedControllerEnable);
367                spinnerPanel.setVisible(false);
368                speedSpinner.setEnabled(false);
369                return;
370                
371            case SLIDERDISPLAY:
372                // normal, drop through
373                break;
374            default:
375                jmri.util.LoggingUtil.warnOnce(log, "Unexpected displaySlider = {}", displaySlider);
376                break;
377        }
378        sliderPanel.setVisible(true);
379        speedSlider.setEnabled(speedControllerEnable);
380        spinnerPanel.setVisible(false);
381        speedSpinner.setEnabled(false);
382        speedSliderContinuousPanel.setVisible(false);
383        speedSliderContinuous.setEnabled(false);        
384    }
385
386    /**
387     * Get the value indicating what speed input we're displaying
388     *
389     * @return SLIDERDISPLAY, STEPDISPLAY or SLIDERDISPLAYCONTINUOUS
390     */
391    public int getDisplaySlider() {
392        return _displaySlider;
393    }
394
395    /**
396     * Provide direct access to speed slider for
397     * scripting.
398     * @return the speed slider
399     */
400    public JSlider getSpeedSlider() {
401        return speedSlider;
402    }
403
404    /**
405     * Set real-time tracking of speed slider, or not
406     *
407     * @param track boolean value, true to track, false to set speed on unclick
408     */
409    public void setTrackSlider(boolean track) {
410        trackSlider = track;
411    }
412
413    /**
414     * Get status of real-time speed slider tracking
415     *
416     * @return true if slider is tracking.
417     */
418    public boolean getTrackSlider() {
419        return trackSlider;
420    }
421
422    /**
423     * Set hiding speed step selector (or not)
424     *
425     * @param hide boolean value, true to hide, false to show
426     */
427    public void setHideSpeedStep(boolean hide) {
428        hideSpeedStep = hide;
429        this.speedStepBox.setVisible(! hideSpeedStep);
430    }
431
432    /**
433     * Get status of hiding  speed step selector
434     *
435     * @return true if speed step selector is hiden.
436     */
437    public boolean getHideSpeedStep() {
438        return hideSpeedStep;
439    }
440
441    /**
442     * Set the GUI to match that the loco speed.
443     *
444     *
445     * @param speedIncrement The throttle back end's speed increment value - %
446     *                       increase for each speed step.
447     * @param speed          The speed value of the loco.
448     */
449    private void setSpeedValues(float speedIncrement, float speed) {
450        //This is an internal speed adjustment
451        internalAdjust = true;
452        //Translate the speed sent in to the max allowed by any set speed limit
453        speedSlider.setValue(java.lang.Math.round(speed / speedIncrement));
454        log.debug("SpeedSlider value: {}", speedSlider.getValue());
455        // Spinner Speed should be the raw integer speed value
456        speedSpinnerModel.setValue(speedSlider.getValue());        
457        if (forwardButton.isSelected()) {
458            speedSliderContinuous.setValue(( speedSlider.getValue()));
459        } else {
460            speedSliderContinuous.setValue(-( speedSlider.getValue()));
461        }
462        
463        stopButton.setSelected((speed == -1 ));
464        idleButton.setSelected((speed == 0 ));
465        internalAdjust = false;
466    }
467
468    private GridBagConstraints makeDefaultGridBagConstraints() {
469        GridBagConstraints constraints = new GridBagConstraints();
470        constraints.anchor = GridBagConstraints.CENTER;
471        constraints.fill = GridBagConstraints.BOTH;
472        constraints.gridheight = 1;
473        constraints.gridwidth = 1;
474        constraints.ipadx = 0;
475        constraints.ipady = 0;
476        constraints.insets = new Insets(2, 2, 2, 2);
477        constraints.weightx = 1;
478        constraints.weighty = 1;
479        constraints.gridx = 0;
480        constraints.gridy = 0;
481
482        return constraints;
483    }
484
485    private void layoutTopButtonPanel() {
486        GridBagConstraints constraints = makeDefaultGridBagConstraints();
487
488        constraints.gridx = 0;
489        constraints.gridy = 0;
490        constraints.fill = GridBagConstraints.HORIZONTAL;
491        topButtonPanel.add(speedStepBox, constraints);
492    }
493
494    private void layoutButtonPanel() {
495        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
496        GridBagConstraints constraints = makeDefaultGridBagConstraints();
497        if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
498            resizeButtons();
499            constraints.insets =  new Insets(0, 0, 0, 0);
500            constraints.gridheight = 2;
501            constraints.gridwidth = 2;
502            constraints.gridy = 0;
503            constraints.gridx = 0;
504            buttonPanel.add(reverseButton, constraints);
505            constraints.gridx = 3;
506            buttonPanel.add(forwardButton, constraints);
507
508            constraints.gridheight = 1;
509            constraints.gridwidth = 1;
510            constraints.gridx = 2;
511            constraints.gridy = 0;
512            buttonPanel.add(idleButton, constraints);
513            constraints.gridy = 1;
514            buttonPanel.add(stopButton, constraints);
515        } else {
516            constraints.fill = GridBagConstraints.NONE;
517            constraints.gridy = 1;
518            buttonPanel.add(forwardButton, constraints);
519            constraints.gridy = 2;
520            buttonPanel.add(reverseButton, constraints);
521            constraints.gridy = 3;
522            buttonPanel.add(idleButton, constraints);
523            constraints.gridy = 4;
524            buttonPanel.add(stopButton, constraints);
525        }
526    }
527
528    private void resizeButtons() {
529        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
530        int w = buttonPanel.getWidth();
531        int h = buttonPanel.getHeight();
532        if ((buttonPanel.getWidth() <= 0 || buttonPanel.getHeight() <= 0)
533                || !(preferences.isUsingExThrottle() && preferences.isUsingLargeSpeedSlider()) ){
534            w = DEFAULT_BUTTON_SIZE * 5;
535            h = DEFAULT_BUTTON_SIZE * 2;
536        }
537        float f = Math.min( Math.floorDiv(w*2,5), h );
538        if (forwardButtonSvgIcon != null ) {
539            forwardButton.setIcon(scaleTo(forwardButtonSvgIcon, f));
540        } else {
541            forwardButton.setIcon(scaleTo(forwardButtonImageIcon, (int)f));
542        }
543        if (forwardSelectedButtonSvgIcon != null) {
544            forwardButton.setSelectedIcon(scaleTo(forwardSelectedButtonSvgIcon, f));
545        } else {
546            forwardButton.setSelectedIcon(scaleTo(forwardSelectedButtonImageIcon, (int)f));
547        }
548        if (forwardRollButtonSvgIcon != null) {
549            forwardButton.setRolloverIcon(scaleTo(forwardRollButtonSvgIcon, f));
550        } else {
551            forwardButton.setRolloverIcon(scaleTo(forwardRollButtonImageIcon, (int)f));
552        }
553        if (reverseButtonSvgIcon != null) {
554            reverseButton.setIcon(scaleTo(reverseButtonSvgIcon, f));
555        } else {
556            reverseButton.setIcon(scaleTo(reverseButtonImageIcon, (int)f));
557        }
558        if (reverseSelectedButtonSvgIcon != null) {
559            reverseButton.setSelectedIcon(scaleTo(reverseSelectedButtonSvgIcon, f));
560        } else {
561            reverseButton.setSelectedIcon(scaleTo(reverseSelectedButtonImageIcon, (int)f));
562        }
563        if (reverseRollButtonSvgIcon != null) {
564            reverseButton.setRolloverIcon(scaleTo(reverseRollButtonSvgIcon, f));
565        } else {
566            reverseButton.setRolloverIcon(scaleTo(reverseRollButtonImageIcon, (int)f));
567        }
568
569        f = Math.min( Math.floorDiv(w,5), h/2 );
570        if (idleButtonSvgIcon != null) {
571            idleButton.setIcon(scaleTo(idleButtonSvgIcon, f));
572        } else {
573            idleButton.setIcon(scaleTo(idleButtonImageIcon, (int)f));
574        }
575        if (idleSelectedButtonSvgIcon != null) {
576            idleButton.setSelectedIcon(scaleTo(idleSelectedButtonSvgIcon, f));
577        } else {
578            idleButton.setSelectedIcon(scaleTo(idleSelectedButtonImageIcon, (int)f));
579        }
580        if (idleRollButtonSvgIcon != null) {
581            idleButton.setRolloverIcon(scaleTo(idleRollButtonSvgIcon, f));
582        } else {
583            idleButton.setRolloverIcon(scaleTo(idleRollButtonImageIcon, (int)f));
584        }
585        if (stopButtonSvgIcon != null) {
586            stopButton.setIcon(scaleTo(stopButtonSvgIcon, f));
587        } else {
588            stopButton.setIcon(scaleTo(stopButtonImageIcon, (int)f));
589        }
590        if (stopSelectedButtonSvgIcon != null) {
591            stopButton.setSelectedIcon(scaleTo(stopSelectedButtonSvgIcon, f));
592        } else {
593            stopButton.setSelectedIcon(scaleTo(stopSelectedButtonImageIcon, (int)f));
594        }
595        if (stopRollButtonSvgIcon != null) {
596            stopButton.setRolloverIcon(scaleTo(stopRollButtonSvgIcon, f));
597        } else {
598            stopButton.setRolloverIcon(scaleTo(stopRollButtonImageIcon, (int)f));
599        }
600    }
601
602    private ImageIcon scaleTo(ImageIcon imic, int s ) {
603        return new ImageIcon(imic.getImage().getScaledInstance(s, s, Image.SCALE_SMOOTH));
604    }
605    
606    MyTranscoder transcoder = new MyTranscoder();
607
608    private ImageIcon scaleTo(Document svgImage, Float f ) {        
609        TranscodingHints hints = new TranscodingHints();
610        hints.put(ImageTranscoder.KEY_WIDTH, f );
611        hints.put(ImageTranscoder.KEY_HEIGHT, f );
612        transcoder.setTranscodingHints(hints);
613        try {
614            transcoder.transcode(new TranscoderInput(svgImage), null);
615        } catch (TranscoderException ex) {
616            // log it, but continue
617            log.debug("Exception while transposing {} : {}", svgImage.getBaseURI(), ex.getMessage());
618        } 
619        return new ImageIcon(transcoder.getImage());
620    }
621
622    private void layoutSliderPanel() {
623        sliderPanel.setLayout(new GridBagLayout());
624        sliderPanel.add(speedSlider, makeDefaultGridBagConstraints());
625    }
626
627    private void layoutSpeedSliderContinuous() {
628        speedSliderContinuousPanel.setLayout(new GridBagLayout());
629        speedSliderContinuousPanel.add(speedSliderContinuous, makeDefaultGridBagConstraints());
630    }
631
632    private void layoutSpinnerPanel() {
633        spinnerPanel.setLayout(new GridBagLayout());
634        GridBagConstraints constraints = makeDefaultGridBagConstraints();
635        constraints.fill = GridBagConstraints.HORIZONTAL;
636        spinnerPanel.add(speedSpinner, constraints);
637    }
638
639    private void setupButton(AbstractButton button, final ThrottlesPreferences preferences, final String message) {
640        button.setHorizontalAlignment(SwingConstants.CENTER);
641        button.setVerticalAlignment(SwingConstants.CENTER);
642        button.setToolTipText(Bundle.getMessage(message));
643        if (preferences != null && preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
644            button.setBorder(null);
645            button.setBorderPainted(false);
646            button.setContentAreaFilled(false);
647            button.setText(null);
648            button.setRolloverEnabled(true);
649        } else {
650            button.setBorder((new JButton()).getBorder());
651            button.setBorderPainted(true);
652            button.setContentAreaFilled(true);
653            button.setText(Bundle.getMessage(message));
654            button.setIcon(null);
655            button.setSelectedIcon(null);
656            button.setRolloverIcon(null);
657            button.setRolloverEnabled(false);
658        }
659    }
660
661    /**
662     * Create, initialize and place GUI components.
663     */
664    private void initGUI(boolean withPopupMenu) {
665        setLayout(new BorderLayout());
666
667        JPanel speedPanel = new JPanel();
668        speedPanel.setLayout(new BorderLayout());
669        speedPanel.setOpaque(false);
670        add(speedPanel, BorderLayout.CENTER);
671
672        topButtonPanel = new JPanel();
673        topButtonPanel.setLayout(new GridBagLayout());
674        speedPanel.add(topButtonPanel, BorderLayout.NORTH);
675
676        speedControlPanel = new JPanel();
677        speedControlPanel.setLayout(new BoxLayout(speedControlPanel, BoxLayout.X_AXIS));
678        speedControlPanel.setOpaque(false);
679        speedPanel.add(speedControlPanel, BorderLayout.CENTER);
680
681        sliderPanel = new JPanel();
682        sliderPanel.setOpaque(false);
683
684        speedSlider = new JSlider(0, intSpeedSteps);
685        speedSlider.setOpaque(false);
686        speedSlider.setValue(0);
687        speedSlider.setFocusable(false);
688        speedSlider.addMouseListener(JmriMouseListener.adapt(new JSliderPreciseMouseAdapter()));
689
690        speedSliderContinuous = new JSlider(-intSpeedSteps, intSpeedSteps);
691        speedSliderContinuous.setValue(0);
692        speedSliderContinuous.setOpaque(false);
693        speedSliderContinuous.setFocusable(false);
694        speedSliderContinuous.addMouseListener(JmriMouseListener.adapt(new JSliderPreciseMouseAdapter()));
695
696        speedSpinner = new JSpinner();
697        speedSpinnerModel = new SpinnerNumberModel(0, 0, intSpeedSteps, 1);
698        speedSpinner.setModel(speedSpinnerModel);
699
700        // customize speed spinner keyboard and focus interactions to not conflict with throttle keyboard shortcuts
701        speedSpinner.getActionMap().put("doNothing", new AbstractAction() {
702            @Override
703            public void actionPerformed(ActionEvent e) {
704                //do nothing
705            }
706        });
707        speedSpinner.getActionMap().put("giveUpFocus", new AbstractAction() {
708            @Override
709            public void actionPerformed(ActionEvent e) {
710               InstanceManager.getDefault(ThrottleFrameManager.class).getCurentThrottleController().getRootPane().requestFocusInWindow();
711            }
712        });
713
714        for ( int i : new ArrayList<>(Arrays.asList(
715                KeyEvent.VK_0, KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_3, KeyEvent.VK_4, KeyEvent.VK_5, KeyEvent.VK_6, KeyEvent.VK_7, KeyEvent.VK_8, KeyEvent.VK_9,
716                KeyEvent.VK_NUMPAD0, KeyEvent.VK_NUMPAD1, KeyEvent.VK_NUMPAD2, KeyEvent.VK_NUMPAD3, KeyEvent.VK_NUMPAD4, KeyEvent.VK_NUMPAD5, KeyEvent.VK_NUMPAD6, KeyEvent.VK_NUMPAD7, KeyEvent.VK_NUMPAD8, KeyEvent.VK_NUMPAD9,
717                KeyEvent.VK_LEFT, KeyEvent.VK_RIGHT, KeyEvent.VK_UP, KeyEvent.VK_DOWN,
718                KeyEvent.VK_DELETE, KeyEvent.VK_BACK_SPACE
719        ))) {
720            speedSpinner.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(i, 0, true), "doNothing");
721            speedSpinner.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(i, 0, false), "doNothing");
722        }
723        speedSpinner.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "giveUpFocus");
724        speedSpinner.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "giveUpFocus");
725
726        EnumSet<SpeedStepMode> speedStepModes = throttleManager.supportedSpeedModes();
727        speedStepBox = new JComboBox<>(speedStepModes.toArray(SpeedStepMode[]::new));
728
729        forwardButton = new JRadioButton();
730        reverseButton = new JRadioButton();
731        try {
732            forwardButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/dirFwdOff.svg").toString());
733        } catch (Exception ex) {
734            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
735            forwardButtonSvgIcon = null;
736            forwardButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/dirFwdOff64.png"));
737        }
738        try {
739            forwardSelectedButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/dirFwdOn.svg").toString());
740        } catch (Exception ex) {
741            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
742            forwardSelectedButtonSvgIcon = null;
743            forwardSelectedButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/dirFwdOn64.png"));
744        }
745        try {
746            forwardRollButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/dirFwdRoll.svg").toString());
747        } catch (Exception ex) {
748            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
749            forwardRollButtonSvgIcon = null;
750            forwardRollButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/dirFwdRoll64.png"));
751        }
752        try {
753            reverseButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/dirBckOff.svg").toString());
754        } catch (Exception ex) {
755            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
756            reverseButtonSvgIcon = null;
757            reverseButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/dirBckOff64.png"));
758        }
759        try {
760            reverseSelectedButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/dirBckOn.svg").toString());
761        } catch (Exception ex) {
762            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
763            reverseSelectedButtonSvgIcon = null;
764            reverseSelectedButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/dirBckOn64.png"));
765        }
766        try {
767            reverseRollButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/dirBckRoll.svg").toString());
768        } catch (Exception ex) {
769            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
770            reverseRollButtonSvgIcon = null;
771            reverseRollButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/dirBckRoll64.png"));
772        }
773        
774        speedLabelVerticalImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/labelArrowVertical.png"));
775        speedLabelHorizontalImageIcon  = new ImageIcon(FileUtil.findURL("resources/icons/throttles/labelArrowHorizontal.png"));
776
777        layoutSliderPanel();
778        speedControlPanel.add(sliderPanel);
779        speedSlider.setOrientation(JSlider.VERTICAL);
780        speedSlider.setMajorTickSpacing(maxSpeed / 2);
781
782        // remove old actions
783        speedSlider.addChangeListener((ChangeEvent e) -> {
784            if (!internalAdjust) {
785                boolean doIt = false;
786                if (!speedSlider.getValueIsAdjusting()) {
787                    doIt = true;
788                    lastTrackedSliderMovementTime = System.currentTimeMillis() - trackSliderMinInterval;
789                } else if (trackSlider
790                        && System.currentTimeMillis() - lastTrackedSliderMovementTime >= trackSliderMinInterval) {
791                    doIt = true;
792                    lastTrackedSliderMovementTime = System.currentTimeMillis();
793                }
794                if (doIt) {
795                    float newSpeed = (speedSlider.getValue() / (intSpeedSteps * 1.0f));
796                    if (log.isDebugEnabled()) {
797                        log.debug("stateChanged: slider pos: {} speed: {}", speedSlider.getValue(), newSpeed);
798                    }
799                    if (sliderPanel.isVisible() && throttle != null) {
800                        throttle.setSpeedSetting(newSpeed);
801                    }
802                    speedSpinnerModel.setValue(speedSlider.getValue());
803                    if (forwardButton.isSelected()) {
804                        speedSliderContinuous.setValue(( speedSlider.getValue()));
805                    } else {
806                        speedSliderContinuous.setValue(-( speedSlider.getValue()));
807                    }                    
808                }
809            }
810        });
811
812        speedSliderContinuousPanel = new JPanel();
813        layoutSpeedSliderContinuous();
814
815        speedControlPanel.add(speedSliderContinuousPanel);
816        speedSliderContinuous.setOrientation(JSlider.VERTICAL);
817        speedSliderContinuous.setMajorTickSpacing(maxSpeed / 2);
818        // remove old actions
819        speedSliderContinuous.addChangeListener((ChangeEvent e) -> {
820            if (!internalAdjust) {
821                boolean doIt = false;
822                if (!speedSliderContinuous.getValueIsAdjusting()) {
823                    doIt = true;
824                    lastTrackedSliderMovementTime = System.currentTimeMillis() - trackSliderMinInterval;
825                } else if (trackSlider
826                        && System.currentTimeMillis() - lastTrackedSliderMovementTime >= trackSliderMinInterval) {
827                    doIt = true;
828                    lastTrackedSliderMovementTime = System.currentTimeMillis();
829                }
830                if (doIt) {
831                    float newSpeed = (java.lang.Math.abs(speedSliderContinuous.getValue()) / (intSpeedSteps * 1.0f));
832                    boolean newDir = (speedSliderContinuous.getValue() >= 0);
833                    if (log.isDebugEnabled()) {
834                        log.debug("stateChanged: slider pos: {} speed: {} dir: {}", speedSliderContinuous.getValue(), newSpeed, newDir);
835                    }
836                    if (speedSliderContinuousPanel.isVisible() && throttle != null) {
837                        throttle.setSpeedSetting(newSpeed);
838                        if ((newSpeed > 0) && (newDir != forwardButton.isSelected())) {
839                            throttle.setIsForward(newDir);
840                        }
841                    }
842                    speedSpinnerModel.setValue(java.lang.Math.abs(speedSliderContinuous.getValue()));
843                    speedSlider.setValue(java.lang.Math.abs(speedSliderContinuous.getValue()));                    
844                }
845            }
846        });
847        computeLabelsTable();
848        updateSlidersLabelDisplay();
849
850        spinnerPanel = new JPanel();
851        layoutSpinnerPanel();
852
853        speedControlPanel.add(spinnerPanel);
854
855        // remove old actions
856        speedSpinner.addChangeListener((ChangeEvent e) -> {
857            if (!internalAdjust) {
858                float newSpeed = ((Integer) speedSpinner.getValue()).floatValue() / (intSpeedSteps * 1.0f);
859                if (log.isDebugEnabled()) {
860                    log.debug("stateChanged: spinner pos: {} speed: {}", speedSpinner.getValue(), newSpeed);
861                }
862                if (throttle != null) {
863                    if (spinnerPanel.isVisible()) {
864                        throttle.setSpeedSetting(newSpeed);
865                    }
866                    speedSlider.setValue(((Integer) speedSpinner.getValue()));
867                    if (forwardButton.isSelected()) {
868                        speedSliderContinuous.setValue(((Integer) speedSpinner.getValue()));
869                    } else {
870                        speedSliderContinuous.setValue(-((Integer) speedSpinner.getValue()));
871                    }                    
872                } else {
873                    log.warn("no throttle object in stateChanged, ignoring change of speed to {}", newSpeed);
874                }
875            }
876        });
877
878        speedStepBox.addActionListener((ActionEvent e) -> {
879            SpeedStepMode s = (SpeedStepMode)speedStepBox.getSelectedItem();
880            setSpeedStepsMode(s);
881            if (throttle != null) {
882              throttle.setSpeedStepMode(s);
883            }
884        });
885
886        buttonPanel = new JPanel();
887        buttonPanel.setLayout(new GridBagLayout());
888        add(buttonPanel, BorderLayout.SOUTH);
889
890        ButtonGroup directionButtons = new ButtonGroup();
891        directionButtons.add(forwardButton);
892        directionButtons.add(reverseButton);
893
894        forwardButton.addActionListener((ActionEvent e) -> {
895            if (throttle != null) {
896              throttle.setIsForward(true);
897            }
898            speedSliderContinuous.setValue(java.lang.Math.abs(speedSliderContinuous.getValue()));            
899        });
900
901        reverseButton.addActionListener((ActionEvent e) -> {
902            if (throttle != null) {
903              throttle.setIsForward(false);
904            }
905            speedSliderContinuous.setValue(-java.lang.Math.abs(speedSliderContinuous.getValue()));            
906        });
907
908        stopButton = new JButton();
909        idleButton = new JButton();
910        try {
911            stopButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/estop.svg").toString());
912        } catch (Exception ex) {
913            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
914            stopButtonSvgIcon = null;
915            stopButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/estop64.png"));
916        }
917        try {
918            stopSelectedButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/estopOn.svg").toString());
919        } catch (Exception ex) {
920            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
921            stopSelectedButtonSvgIcon = null;
922            stopSelectedButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/estopOn64.png"));
923        }
924        try {
925            stopRollButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/estopRoll.svg").toString());
926        } catch (Exception ex) {
927            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
928            stopRollButtonSvgIcon = null;
929            stopRollButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/estopRoll64.png"));
930        }
931        try {
932            idleButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/stop.svg").toString());
933        } catch (Exception ex) {
934            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
935            idleButtonSvgIcon = null;
936            idleButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/stop64.png"));
937        }
938        try {
939            idleSelectedButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/stopOn.svg").toString());
940        } catch (Exception ex) {
941            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
942            idleSelectedButtonSvgIcon = null;
943            idleSelectedButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/stopOn64.png"));
944        }
945        try {
946            idleRollButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/stopRoll.svg").toString());
947        } catch (Exception ex) {
948            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
949            idleRollButtonSvgIcon = null;
950            idleRollButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/stopRoll64.png"));
951        }
952
953        stopButton.addActionListener((ActionEvent e) -> {
954            stop();
955        });
956
957        idleButton.addActionListener((ActionEvent e) -> {
958            idle();
959        });
960
961        addComponentListener(
962                new ComponentAdapter() {
963                    @Override
964                    public void componentResized(ComponentEvent e) {
965                        changeOrientation();
966                    }
967                });
968
969        speedPanel.addComponentListener(
970                new ComponentAdapter() {
971                    @Override
972                    public void componentResized(ComponentEvent e) {
973                        changeFontSizes();
974                    }
975                });
976
977        layoutButtonPanel();
978        layoutTopButtonPanel();
979
980        // Add a mouse listener all components to trigger the popup menu.
981        if (withPopupMenu) {
982            MouseInputAdapterInstaller.installMouseListenerOnAllComponents(new PopupListener(), this);
983        }
984
985        // set by default which speed selection method is on top
986        setSpeedController(_displaySlider);
987    }
988
989  /**
990   * Use the SAXSVGDocumentFactory to parse the given URI into a DOM.
991   *
992   * @param uri The path to the SVG file to read.
993   * @return A Document instance that represents the SVG file.
994   * @throws IOException The file could not be read.
995   */
996    private Document createSVGDocument( String uri ) throws IOException {
997      String parser = XMLResourceDescriptor.getXMLParserClassName();
998      SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory( parser );
999      return factory.createDocument( uri );
1000    }
1001
1002    /**
1003     * Perform an emergency stop.
1004     *
1005     */
1006    public void stop() {
1007        if (this.throttle == null) {
1008            return;
1009        }
1010        internalAdjust = true;
1011        throttle.setSpeedSetting(-1);
1012        speedSlider.setValue(0);
1013        speedSpinnerModel.setValue(0);
1014        speedSliderContinuous.setValue(0);        
1015        internalAdjust = false;
1016    }
1017
1018    private void idle() {
1019        if (this.throttle == null) {
1020            return;
1021        }
1022        internalAdjust = true;
1023        throttle.setSpeedSetting(0);
1024        speedSlider.setValue(0);
1025        speedSpinner.setValue(0);
1026        speedSliderContinuous.setValue(0);
1027        internalAdjust = false;
1028    }
1029
1030    /**
1031     * The user has resized the Frame. Possibly change from Horizontal to
1032     * Vertical layout.
1033     */
1034    private void changeOrientation() {
1035        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1036        if (getWidth() > getHeight()) {
1037            speedSlider.setOrientation(JSlider.HORIZONTAL);                        
1038            speedSliderContinuous.setOrientation(JSlider.HORIZONTAL);
1039            if ( preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon() && preferences.isUsingLargeSpeedSlider() ) {
1040                int bpw = getHeight()*5/2;
1041                if (bpw > getWidth()/2) {
1042                    bpw = getWidth()/2;
1043                }
1044                buttonPanel.setSize(bpw, getHeight());
1045                resizeButtons();
1046            }
1047            remove(buttonPanel);
1048            add(buttonPanel, BorderLayout.EAST);
1049        } else {
1050            speedSlider.setOrientation(JSlider.VERTICAL);           
1051            speedSliderContinuous.setOrientation(JSlider.VERTICAL);                           
1052            if ( preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon() && preferences.isUsingLargeSpeedSlider() ) {
1053                int bph = getWidth()*2/5;
1054                if (bph > getHeight()/2) {
1055                    bph = getHeight()/2;
1056                }
1057                buttonPanel.setSize(getWidth(), bph);
1058                resizeButtons();
1059            }
1060            remove(buttonPanel);
1061            add(buttonPanel, BorderLayout.SOUTH);
1062        }
1063        updateSlidersLabelDisplay();        
1064    }
1065
1066    /**
1067     * A resizing has occurred, so determine the optimum font size for the speed spinner text font.
1068     */
1069    private void changeFontSizes() {
1070        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1071        if ( preferences.isUsingExThrottle() && preferences.isUsingLargeSpeedSlider() ) {
1072            int fontSize = speedSpinner.getFont().getSize();
1073            // fit vertically
1074            int fieldHeight = speedControlPanel.getSize().height;
1075            int stringHeight = speedSpinner.getFontMetrics(speedSpinner.getFont()).getHeight() + 16;
1076            if (stringHeight > fieldHeight) { // component has shrunk vertically
1077                while ((stringHeight > fieldHeight) && (fontSize >= FONT_SIZE_MIN + FONT_INCREMENT)) {
1078                    fontSize -= FONT_INCREMENT;
1079                    Font f = new Font("", Font.PLAIN, fontSize);
1080                    speedSpinner.setFont(f);
1081                    stringHeight = speedSpinner.getFontMetrics(speedSpinner.getFont()).getHeight() + 16;
1082                }
1083            } else { // component has grown vertically
1084                while (fieldHeight - stringHeight > 10) {
1085                    fontSize += FONT_INCREMENT;
1086                    Font f = new Font("", Font.PLAIN, fontSize);
1087                    speedSpinner.setFont(f);
1088                    stringHeight = speedSpinner.getFontMetrics(speedSpinner.getFont()).getHeight() + 16 ;
1089                }
1090            }
1091            // fit horizontally
1092            int fieldWidth = speedControlPanel.getSize().width;
1093            int stringWidth = speedSpinner.getFontMetrics(speedSpinner.getFont()).stringWidth(LONGEST_SS_STRING) + 24 ;
1094            while ((stringWidth > fieldWidth) && (fontSize >= FONT_SIZE_MIN + FONT_INCREMENT)) { // component has shrunk horizontally
1095                fontSize -= FONT_INCREMENT;
1096                Font f = new Font("", Font.PLAIN, fontSize);
1097                speedSpinner.setFont(f);
1098                stringWidth = speedSpinner.getFontMetrics(speedSpinner.getFont()).stringWidth(LONGEST_SS_STRING) + 24 ;
1099            }
1100            speedSpinner.setMinimumSize(new Dimension(stringWidth,stringHeight)); //not sure why this helps here, required
1101        }
1102    }
1103
1104    /**
1105     * Intended for throttle scripting
1106     *
1107     * @param fwd direction: true for forward; false for reverse.
1108     */
1109    public void setForwardDirection(boolean fwd) {
1110        if (fwd) {
1111            if (forwardButton.isEnabled()) {
1112                forwardButton.doClick();
1113            } else {
1114                log.error("setForwardDirection(true) with forwardButton disabled, failed");
1115            }
1116        } else {
1117            if (reverseButton.isEnabled()) {
1118                reverseButton.doClick();
1119            } else {
1120                log.error("setForwardDirection(false) with reverseButton disabled, failed");
1121            }
1122        }
1123    }
1124
1125    // update the state of this panel if any of the properties change
1126    @Override
1127    public void propertyChange(PropertyChangeEvent e) {
1128        if (e == null) {
1129            return;
1130        }
1131        log.debug("Property change event received {} / {}", e.getPropertyName(), e.getNewValue());
1132        if (e.getPropertyName().equals(Throttle.SPEEDSETTING)) {
1133            float speed = ((Float) e.getNewValue());
1134            log.debug("Throttle panel speed updated to {} increment {}", speed,
1135                    throttle.getSpeedIncrement());
1136            setSpeedValues( throttle.getSpeedIncrement(), speed);
1137            throttleFrameManager.getThrottlesListPanel().getTableModel().fireTableDataChanged();
1138        } else if (e.getPropertyName().equals(Throttle.SPEEDSTEPS)) {
1139            SpeedStepMode steps = (SpeedStepMode)e.getNewValue();
1140            setSpeedStepsMode(steps);
1141        } else if (e.getPropertyName().equals(Throttle.ISFORWARD)) {
1142            boolean Forward = ((Boolean) e.getNewValue());
1143            setIsForward(Forward);
1144            throttleFrameManager.getThrottlesListPanel().getTableModel().fireTableDataChanged();
1145        } else if (e.getPropertyName().equals(switchSliderFunction)) {
1146            if ((Boolean) e.getNewValue()) { // switch only if displaying sliders
1147                updateSlidersLabelDisplay();
1148                if (_displaySlider == SLIDERDISPLAY) {
1149                    setSpeedController(SLIDERDISPLAYCONTINUOUS);
1150                }
1151            } else {
1152                updateSlidersLabelDisplay();
1153                if (_displaySlider == SLIDERDISPLAYCONTINUOUS) {
1154                    setSpeedController(SLIDERDISPLAY);
1155                }
1156            }
1157        } else if (ThrottlesPreferences.prefPopertyName.compareTo(e.getPropertyName()) == 0) {
1158            applyPreferences();
1159        }
1160    }
1161
1162    /**
1163     * Apply current throttles preferences to this panel
1164     */
1165    private void applyPreferences() {
1166        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1167
1168        if (preferences.isUsingExThrottle() && preferences.isUsingLargeSpeedSlider()) {
1169             speedSlider.setUI(new ControlPanelCustomSliderUI(speedSlider));
1170             speedSliderContinuous.setUI(new ControlPanelCustomSliderUI(speedSliderContinuous));
1171             changeFontSizes();
1172        } else {
1173            speedSlider.setUI((new JSlider()).getUI());
1174            speedSliderContinuous.setUI((new JSlider()).getUI());
1175            speedSpinner.setFont(new JSpinner().getFont());
1176        }
1177        updateSlidersLabelDisplay();
1178
1179        setupButton(stopButton, preferences, "ButtonEStop");
1180        setupButton(idleButton, preferences, "ButtonIdle");
1181        setupButton(forwardButton, preferences, "ButtonForward");
1182        setupButton(reverseButton, preferences, "ButtonReverse");
1183        buttonPanel.removeAll();
1184        layoutButtonPanel();
1185        if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
1186            changeOrientation(); // force buttons resizing
1187        }
1188        
1189        setHideSpeedStep(preferences.isUsingExThrottle() && preferences.isHidingSpeedStepSelector());        
1190    }
1191
1192    /**
1193     * A PopupListener to handle mouse clicks and releases. Handles the popup
1194     * menu.
1195     */
1196    private class PopupListener extends JmriMouseAdapter {
1197        /**
1198         * If the event is the popup trigger, which is dependent on the
1199         * platform, present the popup menu.
1200         * @param e The JmriMouseEvent causing the action.
1201         */
1202        @Override
1203        public void mouseClicked(JmriMouseEvent e) {
1204            checkTrigger(e);
1205        }
1206
1207        /**
1208         * If the event is the popup trigger, which is dependent on the
1209         * platform, present the popup menu.
1210         * @param e The JmriMouseEvent causing the action.
1211         */
1212        @Override
1213        public void mousePressed(JmriMouseEvent e) {
1214            checkTrigger( e);
1215        }
1216
1217        /**
1218         * If the event is the popup trigger, which is dependent on the
1219         * platform, present the popup menu.
1220         * @param e The JmriMouseEvent causing the action.
1221         */
1222        @Override
1223        public void mouseReleased(JmriMouseEvent e) {
1224            checkTrigger( e);
1225        }
1226
1227        private void checkTrigger( JmriMouseEvent e) {
1228            if (e.isPopupTrigger()) {
1229                initPopupMenu();
1230                popupMenu.show(e.getComponent(), e.getX(), e.getY());
1231            }
1232        }
1233    }
1234
1235    private void initPopupMenu() {
1236        if (popupMenu == null) {
1237            JMenuItem propertiesMenuItem = new JMenuItem(Bundle.getMessage("ControlPanelProperties"));
1238            propertiesMenuItem.addActionListener((ActionEvent e) -> {
1239                if (propertyEditor == null) {
1240                    propertyEditor = new ControlPanelPropertyEditor(this);
1241                }
1242                propertyEditor.setLocation(MouseInfo.getPointerInfo().getLocation());
1243                propertyEditor.resetProperties();
1244                propertyEditor.setVisible(true);
1245            });
1246            popupMenu = new JPopupMenu();
1247            popupMenu.add(propertiesMenuItem);
1248        }
1249    }
1250
1251    /**
1252     * Collect the prefs of this object into XML Element
1253     * <ul>
1254     * <li> Window prefs
1255     * </ul>
1256     *
1257     *
1258     * @return the XML of this object.
1259     */
1260    public Element getXml() {
1261        Element me = new Element("ControlPanel");
1262        me.setAttribute("displaySpeedSlider", String.valueOf(this._displaySlider));
1263        me.setAttribute("trackSlider", String.valueOf(this.trackSlider));
1264        me.setAttribute("trackSliderMinInterval", String.valueOf(this.trackSliderMinInterval));
1265        me.setAttribute("switchSliderOnFunction", switchSliderFunction != null ? switchSliderFunction : "Fxx");
1266        me.setAttribute("hideSpeedStep", String.valueOf(this.hideSpeedStep));
1267        return me;
1268    }
1269
1270    /**
1271     * Set the preferences based on the XML Element.
1272     * <ul>
1273     * <li> Window prefs
1274     * </ul>
1275     *
1276     *
1277     * @param e The Element for this object.
1278     */
1279    public void setXml(Element e) {
1280        internalAdjust = true;
1281        try {
1282            this.setSpeedController(e.getAttribute("displaySpeedSlider").getIntValue());
1283        } catch (org.jdom2.DataConversionException ex) {
1284            log.error("DataConverstionException in setXml", ex);
1285            // in this case, recover by displaying the speed slider.
1286            this.setSpeedController(SLIDERDISPLAY);
1287        }
1288        Attribute tsAtt = e.getAttribute("trackSlider");
1289        if (tsAtt != null) {
1290            try {
1291                trackSlider = tsAtt.getBooleanValue();
1292            } catch (org.jdom2.DataConversionException ex) {
1293                trackSlider = trackSliderDefault;
1294            }
1295        } else {
1296            trackSlider = trackSliderDefault;
1297        }
1298        Attribute tsmiAtt = e.getAttribute("trackSliderMinInterval");
1299        if (tsmiAtt != null) {
1300            try {
1301                trackSliderMinInterval = tsmiAtt.getLongValue();
1302            } catch (org.jdom2.DataConversionException ex) {
1303                trackSliderMinInterval = trackSliderMinIntervalDefault;
1304            }
1305            if (trackSliderMinInterval < trackSliderMinIntervalMin) {
1306                trackSliderMinInterval = trackSliderMinIntervalMin;
1307            } else if (trackSliderMinInterval > trackSliderMinIntervalMax) {
1308                trackSliderMinInterval = trackSliderMinIntervalMax;
1309            }
1310        } else {
1311            trackSliderMinInterval = trackSliderMinIntervalDefault;
1312        }
1313        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);        
1314        Attribute hssAtt = e.getAttribute("hideSpeedStep");
1315        if (hssAtt != null) {
1316            try {
1317                setHideSpeedStep ( hssAtt.getBooleanValue() );
1318            } catch (org.jdom2.DataConversionException ex) {
1319                setHideSpeedStep ( preferences.isUsingExThrottle() && preferences.isHidingSpeedStepSelector() );
1320            }
1321        } else {
1322            setHideSpeedStep ( preferences.isUsingExThrottle() && preferences.isHidingSpeedStepSelector() );
1323        }
1324        if ((prevShuntingFn == null) && (e.getAttribute("switchSliderOnFunction") != null)) {
1325            setSwitchSliderFunction(e.getAttribute("switchSliderOnFunction").getValue());
1326        }
1327        internalAdjust = false;
1328    }
1329
1330    @Override
1331    public void notifyAddressChosen(LocoAddress l) {
1332    }
1333
1334    @Override
1335    public void notifyRosterEntrySelected(RosterEntry re) {     
1336    }
1337
1338    @Override
1339    public void notifyAddressReleased(LocoAddress la) {     
1340        if (throttle != null) {
1341            throttle.removePropertyChangeListener(this);
1342        }
1343        throttle = null;
1344        if (prevShuntingFn != null) {
1345            setSwitchSliderFunction(prevShuntingFn);
1346            prevShuntingFn = null;
1347        }
1348        setEnabled(false);
1349    }
1350
1351    private void addressThrottleFound() {
1352        setEnabled(true);
1353        setIsForward(throttle.getIsForward());
1354        setSpeedStepsMode(throttle.getSpeedStepMode());
1355        setSpeedValues(throttle.getSpeedIncrement(), throttle.getSpeedSetting());
1356        throttle.addPropertyChangeListener(this);
1357    }
1358
1359    @Override
1360    public void notifyAddressThrottleFound(DccThrottle t) {
1361        log.debug("control panel received new throttle {}", t);
1362        if (throttle != null) {
1363            log.debug("notifyAddressThrottleFound() throttle non null, called for loc {}",t.getLocoAddress());
1364            return;
1365        }
1366        if (isConsist) {
1367            // ignore if is a consist
1368            return;
1369        }
1370        throttle = t;
1371        addressThrottleFound();
1372
1373        if ((addressPanel != null) && (addressPanel.getRosterEntry() != null) && (addressPanel.getRosterEntry().getShuntingFunction() != null)) {
1374            prevShuntingFn = getSwitchSliderFunction();
1375            setSwitchSliderFunction(addressPanel.getRosterEntry().getShuntingFunction());                            
1376        } else {
1377            setSwitchSliderFunction(switchSliderFunction); // reset slider           
1378        }
1379        if (log.isDebugEnabled()) {
1380            jmri.DccLocoAddress Address = (jmri.DccLocoAddress) throttle.getLocoAddress();
1381            log.debug("new address is {}", Address.toString());
1382        }
1383    }
1384
1385    @Override
1386    public void notifyConsistAddressChosen(LocoAddress l) {
1387        notifyAddressChosen(l);
1388    }
1389
1390    @Override
1391    public void notifyConsistAddressReleased(LocoAddress la) {
1392        notifyAddressReleased(la);
1393        isConsist = false;
1394    }
1395
1396    @Override
1397    public void notifyConsistAddressThrottleFound(DccThrottle t) {
1398        log.debug("control panel received consist throttle {}", t);
1399        isConsist = true;
1400        throttle = t;
1401        addressThrottleFound();
1402    }
1403
1404    public void setSwitchSliderFunction(String fn) {
1405        switchSliderFunction = fn;
1406        if ((switchSliderFunction == null) || (switchSliderFunction.length() == 0)) {
1407            return;
1408        }
1409        if ((throttle != null) && (_displaySlider != STEPDISPLAY)) { // Update UI depending on function state
1410            try {
1411                // this uses reflection because the user is allowed to name a
1412                // throttle function that triggers this action.
1413                java.lang.reflect.Method getter = throttle.getClass().getMethod("get" + switchSliderFunction, (Class[]) null);
1414
1415                Boolean state = (Boolean) getter.invoke(throttle, (Object[]) null);
1416                if (state) {
1417                    setSpeedController(SLIDERDISPLAYCONTINUOUS);
1418                } else {
1419                    setSpeedController(SLIDERDISPLAY);
1420                }
1421
1422            } catch (IllegalAccessException|NoSuchMethodException|java.lang.reflect.InvocationTargetException ex) {
1423                log.debug("Exception in setSwitchSliderFunction: {} while looking for function {}", ex, switchSliderFunction);
1424            }
1425        }
1426    }
1427    
1428
1429    private void computeLabelsTable() {
1430        defaultLabelTable = new HashMap<>(5);
1431        defaultLabelTable.put(maxSpeed / 2, new JLabel("50%"));
1432        defaultLabelTable.put(maxSpeed, new JLabel("100%"));        
1433        defaultLabelTable.put(0, new JLabel(Bundle.getMessage("ButtonStop")));
1434        defaultLabelTable.put(-maxSpeed / 2, new JLabel("-50%"));
1435        defaultLabelTable.put(-maxSpeed, new JLabel("-100%"));
1436        
1437        if ((addressPanel != null) && (addressPanel.getRosterEntry() != null) && (addressPanel.getRosterEntry().getAttribute("speedLabels") != null)) {
1438            ObjectMapper mapper = new ObjectMapper();
1439            try {
1440                SpeedLabel[] speedLabels = mapper.readValue(addressPanel.getRosterEntry().getAttribute("speedLabels"), SpeedLabel[].class );
1441                if (speedLabels != null && speedLabels.length>0) {
1442                    verticalLabelMap = new HashMap<>(speedLabels.length *2 );
1443                    horizontalLabelMap = new HashMap<>(speedLabels.length *2 );
1444                    JLabel label;
1445                    for (SpeedLabel sp : speedLabels) {
1446                        label = new JLabel( sp.label, speedLabelVerticalImageIcon, SwingConstants.LEFT );
1447                        label.setVerticalTextPosition(JLabel.CENTER);
1448                        verticalLabelMap.put( sp.value, label);
1449                        verticalLabelMap.put( -sp.value, label);
1450
1451                        label = new JLabel( sp.label, speedLabelHorizontalImageIcon, SwingConstants.LEFT );
1452                        label.setHorizontalTextPosition(JLabel.CENTER);
1453                        label.setVerticalTextPosition(JLabel.BOTTOM);
1454
1455                        horizontalLabelMap.put( sp.value, label);
1456                        horizontalLabelMap.put( -sp.value, label);
1457                    }
1458                    updateSlidersLabelDisplay();
1459                }
1460            } catch (JsonProcessingException ex) {
1461                log.error("Exception trying to parse speedLabels attribute from roster entry: {} ", ex.getMessage());                
1462            }                                             
1463        } else {
1464            verticalLabelMap = null;
1465            horizontalLabelMap = null;            
1466        }
1467    }
1468        
1469    // update slider label display depending on context (vertical|horizontal & normal|large)
1470    private void updateSlidersLabelDisplay() {
1471        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1472        Map<Integer, JLabel> labelTable = new HashMap<>(10);
1473        
1474        if ( preferences.isUsingExThrottle() && preferences.isUsingLargeSpeedSlider()) {
1475            speedSlider.setPaintTicks(false);
1476            speedSliderContinuous.setPaintTicks(false);
1477        } else {
1478            speedSlider.setPaintTicks(true);
1479            speedSliderContinuous.setPaintTicks(true);            
1480            labelTable.putAll(defaultLabelTable);                                
1481        }
1482        if ((speedSlider.getOrientation() == JSlider.HORIZONTAL) && (horizontalLabelMap != null)) {
1483            labelTable.putAll(horizontalLabelMap);
1484        } 
1485        if ((speedSlider.getOrientation() == JSlider.VERTICAL) && (verticalLabelMap != null)) {
1486            labelTable.putAll(verticalLabelMap);                 
1487        }
1488        
1489        if (! labelTable.isEmpty()) {
1490            // setLabelTable() only likes Colection which is a HashTable
1491            speedSlider.setLabelTable(new Hashtable<>(labelTable));
1492            speedSliderContinuous.setLabelTable(new Hashtable<>(labelTable));
1493            speedSlider.setPaintLabels(true);
1494            speedSliderContinuous.setPaintLabels(true);
1495        } else {
1496            speedSlider.setPaintLabels(false);
1497            speedSliderContinuous.setPaintLabels(false);
1498        }
1499    }
1500
1501    public String getSwitchSliderFunction() {
1502        return switchSliderFunction;
1503    }
1504
1505    public void saveToRoster(RosterEntry re) {
1506        if (re == null) {
1507            return;
1508        }
1509        if ((re.getShuntingFunction() != null) && (re.getShuntingFunction().compareTo(getSwitchSliderFunction()) != 0)) {
1510            re.setShuntingFunction(getSwitchSliderFunction());
1511        } else if ((re.getShuntingFunction() == null) && (getSwitchSliderFunction() != null)) {
1512            re.setShuntingFunction(getSwitchSliderFunction());
1513        } else {
1514            return;
1515        }
1516        Roster.getDefault().writeRoster();
1517    }
1518
1519    // to handle svg transformation to displayable images
1520    private static class MyTranscoder extends ImageTranscoder {
1521        private BufferedImage image = null;
1522        @Override
1523        public BufferedImage createImage(int w, int h) {
1524            image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
1525            return image;
1526        }
1527        public BufferedImage getImage() {
1528            return image;
1529        }
1530        @Override
1531        public void writeImage(BufferedImage bi, TranscoderOutput to) throws TranscoderException {
1532            //not required here, do nothing
1533        }
1534    }
1535   
1536    // this mouse adapter makes sure to move the slider cursor to precisely where the user clicks
1537    // see https://jmri-developers.groups.io/g/jmri/message/7874
1538    private static class JSliderPreciseMouseAdapter extends JmriMouseAdapter {
1539
1540        @Override
1541        public void mousePressed(JmriMouseEvent e) {
1542            if (e.getButton() == JmriMouseEvent.BUTTON1) {
1543                JSlider sourceSlider = (JSlider) e.getSource();
1544                if (!sourceSlider.isEnabled()) {
1545                    return;
1546                }
1547                BasicSliderUI ui = (BasicSliderUI) sourceSlider.getUI();
1548                int value;
1549                if (sourceSlider.getOrientation() == JSlider.VERTICAL) {
1550                    value = ui.valueForYPosition(e.getY());
1551                } else {
1552                    value = ui.valueForXPosition(e.getX());
1553                }
1554                sourceSlider.setValue(value);
1555            }
1556        }
1557    }
1558    
1559    // For Jackson pasing of roster entry property holding speed labels (if any)
1560    private static class SpeedLabel {
1561        public int value = -1;
1562        public String label = "";      
1563    }
1564
1565    // initialize logging
1566    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ControlPanel.class);
1567}