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