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}