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        setXml(InstanceManager.getDefault(ThrottlesPreferences.class).getControlPanelDefaultXML());
989    }
990
991  /**
992   * Use the SAXSVGDocumentFactory to parse the given URI into a DOM.
993   *
994   * @param uri The path to the SVG file to read.
995   * @return A Document instance that represents the SVG file.
996   * @throws IOException The file could not be read.
997   */
998    private Document createSVGDocument( String uri ) throws IOException {
999      String parser = XMLResourceDescriptor.getXMLParserClassName();
1000      SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory( parser );
1001      return factory.createDocument( uri );
1002    }
1003
1004    /**
1005     * Perform an emergency stop.
1006     *
1007     */
1008    public void stop() {
1009        if (this.throttle == null) {
1010            return;
1011        }
1012        internalAdjust = true;
1013        throttle.setSpeedSetting(-1);
1014        speedSlider.setValue(0);
1015        speedSpinnerModel.setValue(0);
1016        speedSliderContinuous.setValue(0);        
1017        internalAdjust = false;
1018    }
1019
1020    private void idle() {
1021        if (this.throttle == null) {
1022            return;
1023        }
1024        internalAdjust = true;
1025        throttle.setSpeedSetting(0);
1026        speedSlider.setValue(0);
1027        speedSpinner.setValue(0);
1028        speedSliderContinuous.setValue(0);
1029        internalAdjust = false;
1030    }
1031
1032    /**
1033     * The user has resized the Frame. Possibly change from Horizontal to
1034     * Vertical layout.
1035     */
1036    private void changeOrientation() {
1037        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1038        if (getWidth() > getHeight()) {
1039            speedSlider.setOrientation(JSlider.HORIZONTAL);                        
1040            speedSliderContinuous.setOrientation(JSlider.HORIZONTAL);
1041            if ( preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon() && preferences.isUsingLargeSpeedSlider() ) {
1042                int bpw = getHeight()*5/2;
1043                if (bpw > getWidth()/2) {
1044                    bpw = getWidth()/2;
1045                }
1046                buttonPanel.setSize(bpw, getHeight());
1047                resizeButtons();
1048            }
1049            remove(buttonPanel);
1050            add(buttonPanel, BorderLayout.EAST);
1051        } else {
1052            speedSlider.setOrientation(JSlider.VERTICAL);           
1053            speedSliderContinuous.setOrientation(JSlider.VERTICAL);                           
1054            if ( preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon() && preferences.isUsingLargeSpeedSlider() ) {
1055                int bph = getWidth()*2/5;
1056                if (bph > getHeight()/2) {
1057                    bph = getHeight()/2;
1058                }
1059                buttonPanel.setSize(getWidth(), bph);
1060                resizeButtons();
1061            }
1062            remove(buttonPanel);
1063            add(buttonPanel, BorderLayout.SOUTH);
1064        }
1065        updateSlidersLabelDisplay();        
1066    }
1067
1068    /**
1069     * A resizing has occurred, so determine the optimum font size for the speed spinner text font.
1070     */
1071    private void changeFontSizes() {
1072        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1073        if ( preferences.isUsingExThrottle() && preferences.isUsingLargeSpeedSlider() ) {
1074            int fontSize = speedSpinner.getFont().getSize();
1075            // fit vertically
1076            int fieldHeight = speedControlPanel.getSize().height;
1077            int stringHeight = speedSpinner.getFontMetrics(speedSpinner.getFont()).getHeight() + 16;
1078            if (stringHeight > fieldHeight) { // component has shrunk vertically
1079                while ((stringHeight > fieldHeight) && (fontSize >= FONT_SIZE_MIN + FONT_INCREMENT)) {
1080                    fontSize -= FONT_INCREMENT;
1081                    Font f = new Font("", Font.PLAIN, fontSize);
1082                    speedSpinner.setFont(f);
1083                    stringHeight = speedSpinner.getFontMetrics(speedSpinner.getFont()).getHeight() + 16;
1084                }
1085            } else { // component has grown vertically
1086                while (fieldHeight - stringHeight > 10) {
1087                    fontSize += FONT_INCREMENT;
1088                    Font f = new Font("", Font.PLAIN, fontSize);
1089                    speedSpinner.setFont(f);
1090                    stringHeight = speedSpinner.getFontMetrics(speedSpinner.getFont()).getHeight() + 16 ;
1091                }
1092            }
1093            // fit horizontally
1094            int fieldWidth = speedControlPanel.getSize().width;
1095            int stringWidth = speedSpinner.getFontMetrics(speedSpinner.getFont()).stringWidth(LONGEST_SS_STRING) + 24 ;
1096            while ((stringWidth > fieldWidth) && (fontSize >= FONT_SIZE_MIN + FONT_INCREMENT)) { // component has shrunk horizontally
1097                fontSize -= FONT_INCREMENT;
1098                Font f = new Font("", Font.PLAIN, fontSize);
1099                speedSpinner.setFont(f);
1100                stringWidth = speedSpinner.getFontMetrics(speedSpinner.getFont()).stringWidth(LONGEST_SS_STRING) + 24 ;
1101            }
1102            speedSpinner.setMinimumSize(new Dimension(stringWidth,stringHeight)); //not sure why this helps here, required
1103        }
1104    }
1105
1106    /**
1107     * Intended for throttle scripting
1108     *
1109     * @param fwd direction: true for forward; false for reverse.
1110     */
1111    public void setForwardDirection(boolean fwd) {
1112        if (fwd) {
1113            if (forwardButton.isEnabled()) {
1114                forwardButton.doClick();
1115            } else {
1116                log.error("setForwardDirection(true) with forwardButton disabled, failed");
1117            }
1118        } else {
1119            if (reverseButton.isEnabled()) {
1120                reverseButton.doClick();
1121            } else {
1122                log.error("setForwardDirection(false) with reverseButton disabled, failed");
1123            }
1124        }
1125    }
1126
1127    // update the state of this panel if any of the properties change
1128    @Override
1129    public void propertyChange(PropertyChangeEvent e) {
1130        if (e == null) {
1131            return;
1132        }
1133        log.debug("Property change event received {} / {}", e.getPropertyName(), e.getNewValue());
1134        if (e.getPropertyName().equals(Throttle.SPEEDSETTING)) {
1135            float speed = ((Float) e.getNewValue());
1136            log.debug("Throttle panel speed updated to {} increment {}", speed,
1137                    throttle.getSpeedIncrement());
1138            setSpeedValues( throttle.getSpeedIncrement(), speed);
1139            throttleFrameManager.getThrottlesListPanel().getTableModel().fireTableDataChanged();
1140        } else if (e.getPropertyName().equals(Throttle.SPEEDSTEPS)) {
1141            SpeedStepMode steps = (SpeedStepMode)e.getNewValue();
1142            setSpeedStepsMode(steps);
1143        } else if (e.getPropertyName().equals(Throttle.ISFORWARD)) {
1144            boolean Forward = ((Boolean) e.getNewValue());
1145            setIsForward(Forward);
1146            throttleFrameManager.getThrottlesListPanel().getTableModel().fireTableDataChanged();
1147        } else if (e.getPropertyName().equals(switchSliderFunction)) {
1148            if ((Boolean) e.getNewValue()) { // switch only if displaying sliders
1149                updateSlidersLabelDisplay();
1150                if (_displaySlider == SLIDERDISPLAY) {
1151                    setSpeedController(SLIDERDISPLAYCONTINUOUS);
1152                }
1153            } else {
1154                updateSlidersLabelDisplay();
1155                if (_displaySlider == SLIDERDISPLAYCONTINUOUS) {
1156                    setSpeedController(SLIDERDISPLAY);
1157                }
1158            }
1159        } else if (ThrottlesPreferences.prefPopertyName.compareTo(e.getPropertyName()) == 0) {
1160            applyPreferences();
1161        }
1162    }
1163
1164    /**
1165     * Apply current throttles preferences to this panel
1166     */
1167    private void applyPreferences() {
1168        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1169
1170        if (preferences.isUsingExThrottle() && preferences.isUsingLargeSpeedSlider()) {
1171             speedSlider.setUI(new ControlPanelCustomSliderUI(speedSlider));
1172             speedSliderContinuous.setUI(new ControlPanelCustomSliderUI(speedSliderContinuous));
1173             changeFontSizes();
1174        } else {
1175            speedSlider.setUI((new JSlider()).getUI());
1176            speedSliderContinuous.setUI((new JSlider()).getUI());
1177            speedSpinner.setFont(new JSpinner().getFont());
1178        }
1179        updateSlidersLabelDisplay();
1180
1181        setupButton(stopButton, preferences, "ButtonEStop");
1182        setupButton(idleButton, preferences, "ButtonIdle");
1183        setupButton(forwardButton, preferences, "ButtonForward");
1184        setupButton(reverseButton, preferences, "ButtonReverse");
1185        buttonPanel.removeAll();
1186        layoutButtonPanel();
1187        if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
1188            changeOrientation(); // force buttons resizing
1189        }
1190        
1191        setHideSpeedStep(preferences.isUsingExThrottle() && preferences.isHidingSpeedStepSelector());        
1192    }
1193
1194    /**
1195     * A PopupListener to handle mouse clicks and releases. Handles the popup
1196     * menu.
1197     */
1198    private class PopupListener extends JmriMouseAdapter {
1199        /**
1200         * If the event is the popup trigger, which is dependent on the
1201         * platform, present the popup menu.
1202         * @param e The JmriMouseEvent causing the action.
1203         */
1204        @Override
1205        public void mouseClicked(JmriMouseEvent e) {
1206            checkTrigger(e);
1207        }
1208
1209        /**
1210         * If the event is the popup trigger, which is dependent on the
1211         * platform, present the popup menu.
1212         * @param e The JmriMouseEvent causing the action.
1213         */
1214        @Override
1215        public void mousePressed(JmriMouseEvent e) {
1216            checkTrigger( e);
1217        }
1218
1219        /**
1220         * If the event is the popup trigger, which is dependent on the
1221         * platform, present the popup menu.
1222         * @param e The JmriMouseEvent causing the action.
1223         */
1224        @Override
1225        public void mouseReleased(JmriMouseEvent e) {
1226            checkTrigger( e);
1227        }
1228
1229        private void checkTrigger( JmriMouseEvent e) {
1230            if (e.isPopupTrigger()) {
1231                initPopupMenu();
1232                popupMenu.show(e.getComponent(), e.getX(), e.getY());
1233            }
1234        }
1235    }
1236
1237    private void initPopupMenu() {
1238        if (popupMenu == null) {
1239            JMenuItem propertiesMenuItem = new JMenuItem(Bundle.getMessage("ControlPanelProperties"));
1240            propertiesMenuItem.addActionListener((ActionEvent e) -> {
1241                if (propertyEditor == null) {
1242                    propertyEditor = new ControlPanelPropertyEditor(this);
1243                }
1244                propertyEditor.setLocation(MouseInfo.getPointerInfo().getLocation());
1245                propertyEditor.resetProperties();
1246                propertyEditor.setVisible(true);
1247            });
1248            popupMenu = new JPopupMenu();
1249            popupMenu.add(propertiesMenuItem);
1250        }
1251    }
1252
1253    /**
1254     * Collect the prefs of this object into XML Element
1255     * <ul>
1256     * <li> Window prefs
1257     * </ul>
1258     *
1259     *
1260     * @return the XML of this object.
1261     */
1262    public Element getXml() {
1263        Element me = new Element("ControlPanel");
1264        me.setAttribute("displaySpeedSlider", String.valueOf(this._displaySlider));
1265        me.setAttribute("trackSlider", String.valueOf(this.trackSlider));
1266        me.setAttribute("trackSliderMinInterval", String.valueOf(this.trackSliderMinInterval));
1267        me.setAttribute("switchSliderOnFunction", switchSliderFunction != null ? switchSliderFunction : "Fxx");
1268        me.setAttribute("hideSpeedStep", String.valueOf(this.hideSpeedStep));
1269        return me;
1270    }
1271
1272    /**
1273     * Set the preferences based on the XML Element.
1274     * <ul>
1275     * <li> Window prefs
1276     * </ul>
1277     *
1278     * @param e The Element for this object.
1279     */
1280    public void setXml(Element e) {
1281        if (e == null) {
1282            return;
1283        }
1284        internalAdjust = true;
1285        try {
1286            this.setSpeedController(e.getAttribute("displaySpeedSlider").getIntValue());
1287        } catch (org.jdom2.DataConversionException ex) {
1288            log.error("DataConverstionException in setXml", ex);
1289            // in this case, recover by displaying the speed slider.
1290            this.setSpeedController(SLIDERDISPLAY);
1291        }
1292        Attribute tsAtt = e.getAttribute("trackSlider");
1293        if (tsAtt != null) {
1294            try {
1295                trackSlider = tsAtt.getBooleanValue();
1296            } catch (org.jdom2.DataConversionException ex) {
1297                trackSlider = trackSliderDefault;
1298            }
1299        } else {
1300            trackSlider = trackSliderDefault;
1301        }
1302        Attribute tsmiAtt = e.getAttribute("trackSliderMinInterval");
1303        if (tsmiAtt != null) {
1304            try {
1305                trackSliderMinInterval = tsmiAtt.getLongValue();
1306            } catch (org.jdom2.DataConversionException ex) {
1307                trackSliderMinInterval = trackSliderMinIntervalDefault;
1308            }
1309            if (trackSliderMinInterval < trackSliderMinIntervalMin) {
1310                trackSliderMinInterval = trackSliderMinIntervalMin;
1311            } else if (trackSliderMinInterval > trackSliderMinIntervalMax) {
1312                trackSliderMinInterval = trackSliderMinIntervalMax;
1313            }
1314        } else {
1315            trackSliderMinInterval = trackSliderMinIntervalDefault;
1316        }
1317        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);        
1318        Attribute hssAtt = e.getAttribute("hideSpeedStep");
1319        if (hssAtt != null) {
1320            try {
1321                setHideSpeedStep ( hssAtt.getBooleanValue() );
1322            } catch (org.jdom2.DataConversionException ex) {
1323                setHideSpeedStep ( preferences.isUsingExThrottle() && preferences.isHidingSpeedStepSelector() );
1324            }
1325        } else {
1326            setHideSpeedStep ( preferences.isUsingExThrottle() && preferences.isHidingSpeedStepSelector() );
1327        }
1328        if ((prevShuntingFn == null) && (e.getAttribute("switchSliderOnFunction") != null)) {
1329            setSwitchSliderFunction(e.getAttribute("switchSliderOnFunction").getValue());
1330        }
1331        internalAdjust = false;
1332    }
1333
1334    @Override
1335    public void notifyAddressChosen(LocoAddress l) {
1336    }
1337
1338    @Override
1339    public void notifyNewRosterEntryHighlighted(RosterEntry re) {     
1340    }
1341
1342    @Override
1343    public void notifyRosterEntryUpdated(RosterEntry re) {
1344        if ((re != null) && (re.getShuntingFunction() != null)) {
1345            prevShuntingFn = getSwitchSliderFunction();
1346            setSwitchSliderFunction(addressPanel.getRosterEntry().getShuntingFunction());                            
1347        } else {
1348            setSwitchSliderFunction(switchSliderFunction); // reset slider           
1349        }
1350    }
1351
1352    @Override
1353    public void notifyAddressReleased(LocoAddress la) {     
1354        if (throttle != null) {
1355            throttle.removePropertyChangeListener(this);
1356        }
1357        throttle = null;
1358        if (prevShuntingFn != null) {
1359            setSwitchSliderFunction(prevShuntingFn);
1360            prevShuntingFn = null;
1361        }
1362        setEnabled(false);
1363    }
1364
1365    private void addressThrottleFound() {
1366        setEnabled(true);
1367        setIsForward(throttle.getIsForward());
1368        setSpeedStepsMode(throttle.getSpeedStepMode());
1369        setSpeedValues(throttle.getSpeedIncrement(), throttle.getSpeedSetting());
1370        throttle.addPropertyChangeListener(this);
1371    }
1372
1373    @Override
1374    public void notifyAddressThrottleFound(DccThrottle t) {
1375        log.debug("control panel received new throttle {}", t);
1376        if (throttle != null) {
1377            log.debug("notifyAddressThrottleFound() throttle non null, called for loc {}",t.getLocoAddress());
1378            return;
1379        }
1380        if (isConsist) {
1381            // ignore if is a consist
1382            return;
1383        }
1384        throttle = t;
1385        addressThrottleFound();
1386        if (addressPanel!=null) {
1387            notifyRosterEntryUpdated(addressPanel.getRosterEntry() );
1388        } else {
1389            notifyRosterEntryUpdated(null);
1390        }
1391        
1392        if (log.isDebugEnabled()) {
1393            jmri.DccLocoAddress Address = (jmri.DccLocoAddress) throttle.getLocoAddress();
1394            log.debug("new address is {}", Address.toString());
1395        }
1396    }
1397
1398    @Override
1399    public void notifyConsistAddressChosen(LocoAddress l) {
1400        notifyAddressChosen(l);
1401    }
1402
1403    @Override
1404    public void notifyConsistAddressReleased(LocoAddress la) {
1405        notifyAddressReleased(la);
1406        isConsist = false;
1407    }
1408
1409    @Override
1410    public void notifyConsistAddressThrottleFound(DccThrottle t) {
1411        log.debug("control panel received consist throttle {}", t);
1412        isConsist = true;
1413        throttle = t;
1414        addressThrottleFound();
1415    }
1416
1417    public void setSwitchSliderFunction(String fn) {
1418        switchSliderFunction = fn;
1419        if ((switchSliderFunction == null) || (switchSliderFunction.length() == 0)) {
1420            return;
1421        }
1422        if ((throttle != null) && (_displaySlider != STEPDISPLAY)) { // Update UI depending on function state
1423            try {
1424                // this uses reflection because the user is allowed to name a
1425                // throttle function that triggers this action.
1426                java.lang.reflect.Method getter = throttle.getClass().getMethod("get" + switchSliderFunction, (Class[]) null);
1427
1428                Boolean state = (Boolean) getter.invoke(throttle, (Object[]) null);
1429                if (state) {
1430                    setSpeedController(SLIDERDISPLAYCONTINUOUS);
1431                } else {
1432                    setSpeedController(SLIDERDISPLAY);
1433                }
1434
1435            } catch (IllegalAccessException|NoSuchMethodException|java.lang.reflect.InvocationTargetException ex) {
1436                log.debug("Exception in setSwitchSliderFunction: {} while looking for function {}", ex, switchSliderFunction);
1437            }
1438        }
1439    }
1440    
1441
1442    private void computeLabelsTable() {
1443        defaultLabelTable = new HashMap<>(5);
1444        defaultLabelTable.put(maxSpeed / 2, new JLabel("50%"));
1445        defaultLabelTable.put(maxSpeed, new JLabel("100%"));        
1446        defaultLabelTable.put(0, new JLabel(Bundle.getMessage("ButtonStop")));
1447        defaultLabelTable.put(-maxSpeed / 2, new JLabel("-50%"));
1448        defaultLabelTable.put(-maxSpeed, new JLabel("-100%"));
1449        
1450        if ((addressPanel != null) && (addressPanel.getRosterEntry() != null) && (addressPanel.getRosterEntry().getAttribute("speedLabels") != null)) {
1451            ObjectMapper mapper = new ObjectMapper();
1452            try {
1453                SpeedLabel[] speedLabels = mapper.readValue(addressPanel.getRosterEntry().getAttribute("speedLabels"), SpeedLabel[].class );
1454                if (speedLabels != null && speedLabels.length>0) {
1455                    verticalLabelMap = new HashMap<>(speedLabels.length *2 );
1456                    horizontalLabelMap = new HashMap<>(speedLabels.length *2 );
1457                    JLabel label;
1458                    for (SpeedLabel sp : speedLabels) {
1459                        label = new JLabel( sp.label, speedLabelVerticalImageIcon, SwingConstants.LEFT );
1460                        label.setVerticalTextPosition(JLabel.CENTER);
1461                        verticalLabelMap.put( sp.value, label);
1462                        verticalLabelMap.put( -sp.value, label);
1463
1464                        label = new JLabel( sp.label, speedLabelHorizontalImageIcon, SwingConstants.LEFT );
1465                        label.setHorizontalTextPosition(JLabel.CENTER);
1466                        label.setVerticalTextPosition(JLabel.BOTTOM);
1467
1468                        horizontalLabelMap.put( sp.value, label);
1469                        horizontalLabelMap.put( -sp.value, label);
1470                    }
1471                    updateSlidersLabelDisplay();
1472                }
1473            } catch (JsonProcessingException ex) {
1474                log.error("Exception trying to parse speedLabels attribute from roster entry: {} ", ex.getMessage());                
1475            }                                             
1476        } else {
1477            verticalLabelMap = null;
1478            horizontalLabelMap = null;            
1479        }
1480    }
1481        
1482    // update slider label display depending on context (vertical|horizontal & normal|large)
1483    private void updateSlidersLabelDisplay() {
1484        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1485        Map<Integer, JLabel> labelTable = new HashMap<>(10);
1486        
1487        if ( preferences.isUsingExThrottle() && preferences.isUsingLargeSpeedSlider()) {
1488            speedSlider.setPaintTicks(false);
1489            speedSliderContinuous.setPaintTicks(false);
1490        } else {
1491            speedSlider.setPaintTicks(true);
1492            speedSliderContinuous.setPaintTicks(true);            
1493            labelTable.putAll(defaultLabelTable);                                
1494        }
1495        if ((speedSlider.getOrientation() == JSlider.HORIZONTAL) && (horizontalLabelMap != null)) {
1496            labelTable.putAll(horizontalLabelMap);
1497        } 
1498        if ((speedSlider.getOrientation() == JSlider.VERTICAL) && (verticalLabelMap != null)) {
1499            labelTable.putAll(verticalLabelMap);                 
1500        }
1501        
1502        if (! labelTable.isEmpty()) {
1503            // setLabelTable() only likes Colection which is a HashTable
1504            speedSlider.setLabelTable(new Hashtable<>(labelTable));
1505            speedSliderContinuous.setLabelTable(new Hashtable<>(labelTable));
1506            speedSlider.setPaintLabels(true);
1507            speedSliderContinuous.setPaintLabels(true);
1508        } else {
1509            speedSlider.setPaintLabels(false);
1510            speedSliderContinuous.setPaintLabels(false);
1511        }
1512    }
1513
1514    public String getSwitchSliderFunction() {
1515        return switchSliderFunction;
1516    }
1517
1518    public void saveToRoster(RosterEntry re) {
1519        if (re == null) {
1520            return;
1521        }
1522        if ((re.getShuntingFunction() != null) && (re.getShuntingFunction().compareTo(getSwitchSliderFunction()) != 0)) {
1523            re.setShuntingFunction(getSwitchSliderFunction());
1524        } else if ((re.getShuntingFunction() == null) && (getSwitchSliderFunction() != null)) {
1525            re.setShuntingFunction(getSwitchSliderFunction());
1526        } else {
1527            return;
1528        }
1529        Roster.getDefault().writeRoster();
1530    }
1531
1532    // to handle svg transformation to displayable images
1533    private static class MyTranscoder extends ImageTranscoder {
1534        private BufferedImage image = null;
1535        @Override
1536        public BufferedImage createImage(int w, int h) {
1537            image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
1538            return image;
1539        }
1540        public BufferedImage getImage() {
1541            return image;
1542        }
1543        @Override
1544        public void writeImage(BufferedImage bi, TranscoderOutput to) throws TranscoderException {
1545            //not required here, do nothing
1546        }
1547    }
1548   
1549    // this mouse adapter makes sure to move the slider cursor to precisely where the user clicks
1550    // see https://jmri-developers.groups.io/g/jmri/message/7874
1551    private static class JSliderPreciseMouseAdapter extends JmriMouseAdapter {
1552
1553        @Override
1554        public void mousePressed(JmriMouseEvent e) {
1555            if (e.getButton() == JmriMouseEvent.BUTTON1) {
1556                JSlider sourceSlider = (JSlider) e.getSource();
1557                if (!sourceSlider.isEnabled()) {
1558                    return;
1559                }
1560                BasicSliderUI ui = (BasicSliderUI) sourceSlider.getUI();
1561                int value;
1562                if (sourceSlider.getOrientation() == JSlider.VERTICAL) {
1563                    value = ui.valueForYPosition(e.getY());
1564                } else {
1565                    value = ui.valueForXPosition(e.getX());
1566                }
1567                sourceSlider.setValue(value);
1568            }
1569        }
1570    }
1571    
1572    // For Jackson pasing of roster entry property holding speed labels (if any)
1573    private static class SpeedLabel {
1574        public int value = -1;
1575        public String label = "";      
1576    }
1577
1578    // initialize logging
1579    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ControlPanel.class);
1580}