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