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}