001package jmri.jmrit.display.switchboardEditor; 002 003import java.awt.*; 004import java.awt.event.*; 005import java.awt.font.FontRenderContext; 006import java.awt.geom.AffineTransform; 007import java.awt.geom.Point2D; 008import java.awt.image.BufferedImage; 009import java.awt.image.RescaleOp; 010import java.io.File; 011import java.io.IOException; 012 013import javax.annotation.CheckForNull; 014import javax.annotation.Nonnull; 015import javax.imageio.ImageIO; 016import javax.swing.*; 017 018import jmri.InstanceManager; 019import jmri.JmriException; 020import jmri.Light; 021import jmri.LightManager; 022import jmri.Manager; 023import jmri.NamedBean; 024import jmri.NamedBeanHandle; 025import jmri.Sensor; 026import jmri.SensorManager; 027import jmri.Turnout; 028import jmri.TurnoutManager; 029import jmri.jmrit.beantable.AddNewDevicePanel; 030import jmri.util.*; 031import jmri.util.swing.JmriJOptionPane; 032 033/** 034 * Class for a switchboard interface object. 035 * <p> 036 * Contains a JButton or JPanel to control existing turnouts, sensors and 037 * lights. 038 * Separated from SwitchboardEditor.java in 4.12.3 039 * 040 * @author Egbert Broerse Copyright (c) 2017, 2018, 2020, 2021 041 */ 042public class BeanSwitch extends JPanel implements java.beans.PropertyChangeListener, ActionListener { 043 044 private final JButton beanButton = new JButton(); 045 private IconSwitch iconSwitch; 046 private final int _shape; 047 private int square = 75; // outside dimension of graphic, normally < 2*radius 048 private int radius = 50; // max distance in px from center of switch canvas, unit used for relative scaling 049 private double popScale = 1.0d; 050 private SwitchBoardLabelDisplays showUserName = SwitchBoardLabelDisplays.BOTH_NAMES; 051 private Color activeColor = Color.RED; // for testing a separate BeanSwitch 052 private Color inactiveColor = Color.GREEN; 053 Color textColor = Color.BLACK; 054 protected String switchLabel; 055 protected String switchTooltip; 056 protected boolean _text; 057 protected boolean _icon = false; 058 protected boolean _control = false; 059 protected int _showingState = 0; 060 protected String _stateSign; 061 protected String _color; 062 protected String stateClosed = Bundle.getMessage("StateClosedShort"); 063 protected String stateThrown = Bundle.getMessage("StateThrownShort"); 064 065 private final SwitchboardEditor _editor; 066 private char beanTypeChar = 'T'; // initialize now to allow testing 067 private String switchTypeName = "Turnout"; 068 private String manuPrefix = "I"; 069 private final String _switchSysName; 070 private String _switchDisplayName; 071 boolean showToolTip = true; 072 boolean allControlling = true; 073 boolean panelEditable = false; 074 // the associated Bean object 075 private final NamedBean _bname; 076 private NamedBeanHandle<?> namedBean = null; // can be Turnout, Sensor or Light 077 protected jmri.NamedBeanHandleManager nbhm = jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class); 078 private String _uName = "unconnected"; 079 private String _uLabel = ""; // for display, empty if userName == null or showUserName != BOTH_NAMES 080 081 /** 082 * Ctor. 083 * 084 * @param index ordinal of this switch on Switchboard. 085 * @param bean layout object to connect to. 086 * @param switchName descriptive name corresponding with system name to 087 * display in switch tooltip, i.e. LT1. 088 * @param shapeChoice Button, Slider, Key (all drawn on screen) or Icon (sets of graphic files). 089 * @param editor main switchboard editor. 090 */ 091 public BeanSwitch(int index, @CheckForNull NamedBean bean, @Nonnull String switchName, int shapeChoice, @CheckForNull SwitchboardEditor editor) { 092 log.debug("Name = [{}]", switchName); 093 _switchSysName = switchName; 094 _switchDisplayName = switchName; // updated later on if a user name is available 095 _editor = editor; 096 _bname = bean; 097 _shape = shapeChoice; 098 //if (_switchSysName.length() < 3) { // causes unexpected effects? 099 // log.error("Switch name {} too short for a valid system name", switchName); 100 // return; 101 //} 102 sysNameTextBox.setText(switchName); // setting name here allows test of AddNew() 103 boolean hideUnconnected = false; 104 Color backgroundColor = Color.LIGHT_GRAY; 105 if (editor != null) { 106 // get connection 107 manuPrefix = editor.getSwitchManu(); // connection/manufacturer prefix i.e. default) M for MERG 108 switchTypeName = _editor.getSwitchTypeName(); 109 // get display settings 110 hideUnconnected = editor.hideUnconnected(); 111 allControlling = editor.allControlling(); 112 panelEditable = editor.isEditable(); 113 showToolTip = editor.showToolTip(); 114 showUserName = editor.nameDisplay(); 115 radius = editor.getTileSize()/2; // max WxH of canvas inside cell, used as relative unit to draw 116 square = editor.getIconScale(); 117 // get colors 118 textColor = editor.getDefaultTextColorAsColor(); 119 backgroundColor = editor.getDefaultBackgroundColor(); 120 activeColor = editor.getActiveColorAsColor(); 121 inactiveColor = editor.getInactiveColorAsColor(); 122 popScale = _editor.getPaintScale(); 123 } 124 if (bean != null) { 125 _uName = bean.getUserName(); 126 log.debug("Switch userName from bean: {}", _uName); 127 if (_uName == null) { 128 _uName = Bundle.getMessage("NoUserName"); 129 } else if (showUserName == SwitchBoardLabelDisplays.BOTH_NAMES) { // (menu option setting) 130 _uLabel = _uName; 131 } else if (showUserName == SwitchBoardLabelDisplays.USER_NAME) { 132 _switchDisplayName = _uName; // replace system name 133 } 134 } 135 136 switchTooltip = switchName + " (" + _uName + ")"; 137 this.setLayout(new BorderLayout()); // makes JButtons expand to the whole grid cell 138 139 beanTypeChar = _switchSysName.charAt(manuPrefix.length()); // bean type, i.e. L, usually at char(1) 140 // check for space char which might be caused by connection name > 2 chars and/or space in name 141 if (beanTypeChar != 'T' && beanTypeChar != 'S' && beanTypeChar != 'L') { // add if more bean types are supported 142 log.error("invalid char in Switchboard Button \"{}\". Check connection name.", _switchSysName); 143 JmriJOptionPane.showMessageDialog(editor, Bundle.getMessage("ErrorSwitchAddFailed"), 144 Bundle.getMessage("WarningTitle"), 145 JmriJOptionPane.ERROR_MESSAGE); 146 return; 147 } 148 149 log.debug("BeanSwitch graphic tilesize/2 r={} scale={}", radius, square); 150 151 // look for bean to connect to by name 152 log.debug("beanconnect = {}, beantype = {}", manuPrefix, beanTypeChar); 153 try { 154 if (bean != null) { 155 namedBean = nbhm.getNamedBeanHandle(switchName, bean); 156 } 157 } catch (IllegalArgumentException e) { 158 log.error("invalid bean name= \"{}\" in Switchboard Button", switchName); 159 } 160 161 _text = true; // not actually used, web server supports in-browser drawn switches check in 162 _icon = true; // panel.js assigns only text OR icon for a single class such as BeanSwitch 163 // attach shape specific code to this beanSwitch 164 switch (_shape) { 165 case SwitchboardEditor.SLIDER: // slider shape 166 iconSwitch = new IconSwitch(_shape, beanTypeChar); // draw as Graphic2D 167 iconSwitch.setPreferredSize(new Dimension(2*radius, 2*radius)); // tweak layout 168 iconSwitch.positionLabel(0, 5*radius/-8, Component.CENTER_ALIGNMENT, getLabelFontSize(radius, _switchDisplayName)); 169 iconSwitch.positionSubLabel(0, radius/-5, Component.CENTER_ALIGNMENT, getSubLabelFontSize(radius, _uName)); // smaller (system name) 170 this.add(iconSwitch); 171 break; 172 case SwitchboardEditor.KEY: // Maerklin style keyboard 173 iconSwitch = new IconSwitch(_shape, beanTypeChar); // draw as Graphic2D 174 iconSwitch.setPreferredSize(new Dimension(2*radius, 2*radius)); // tweak layout 175 iconSwitch.positionLabel(0, 0, Component.CENTER_ALIGNMENT, getLabelFontSize(radius, _switchDisplayName)); 176 iconSwitch.positionSubLabel(0, 3*radius/10, Component.CENTER_ALIGNMENT, getSubLabelFontSize(radius, _uName)); // smaller (system name) 177 // provide x, y offset, depending on image size and free space 178 this.add(iconSwitch); 179 break; 180 case SwitchboardEditor.SYMBOL: 181 // turnout/sensor/light symbol using image files (selecting image by letter in switch name/label) 182 iconSwitch = new IconSwitch( 183 rootPath + beanTypeChar + "-on-s.png", 184 rootPath + beanTypeChar + "-off-s.png", backgroundColor); 185 iconSwitch.setPreferredSize(new Dimension(2*radius, 2*radius)); 186 switch (beanTypeChar) { 187 case 'T' : 188 iconSwitch.positionLabel(0, 5*radius/-8, Component.CENTER_ALIGNMENT, getLabelFontSize(radius, _switchDisplayName)); 189 iconSwitch.positionSubLabel(0, radius/-4, Component.CENTER_ALIGNMENT, getSubLabelFontSize(radius, _uName)); 190 break; 191 case 'S' : 192 case 'L' : 193 default : 194 iconSwitch.positionLabel(0, 5*radius/-8, Component.CENTER_ALIGNMENT, getLabelFontSize(radius, _switchDisplayName)); 195 iconSwitch.positionSubLabel(0, radius/-3, Component.CENTER_ALIGNMENT, getSubLabelFontSize(radius, _uName)); 196 } 197 // common (in)activecolor etc defined in SwitchboardEditor, retrieved by Servlet 198 this.setBorder(BorderFactory.createLineBorder(backgroundColor, 3)); 199 this.add(iconSwitch); 200 break; 201 case SwitchboardEditor.BUTTON: // 0 = "Button" shape 202 default: 203 _icon = false; 204 beanButton.setText(getSwitchButtonLabel(_switchDisplayName + ": ?")); // initial text to display 205 beanButton.setToolTipText(getSwitchButtonToolTip(switchLabel)); 206 beanButton.setForeground(textColor); 207 beanButton.setOpaque(true); // to show color from the start 208 this.setBorder(BorderFactory.createLineBorder(backgroundColor, 2)); 209 beanButton.addComponentListener(new ComponentAdapter() { 210 @Override 211 public void componentResized(ComponentEvent e) { 212 if ((showUserName == SwitchBoardLabelDisplays.BOTH_NAMES) && (beanButton.getHeight() < 50)) { 213 beanButton.setVerticalAlignment(JLabel.TOP); 214 } else { 215 beanButton.setVerticalAlignment(JLabel.CENTER); //default 216 } 217 } 218 }); 219 beanButton.addMouseListener(new MouseAdapter() { // pass on mouseEvents 220 @Override 221 public void mouseClicked(MouseEvent e) { 222 redispatchToParent(e); 223 } 224 @Override 225 public void mouseReleased(MouseEvent e) { 226 redispatchToParent(e); 227 } 228 @Override 229 public void mousePressed(MouseEvent e) { 230 redispatchToParent(e); 231 } 232 }); 233 beanButton.setMargin(new Insets(4, 1, 2, 1)); 234 this.add(beanButton); 235 break; 236 } 237 // common configuration of graphic switches 238 addMouseListener(new MouseAdapter() { // handled by JPanel 239 @Override 240 public void mouseClicked(MouseEvent me) { 241 operate(me, switchName); 242 } 243 244 @Override 245 public void mouseReleased(MouseEvent me) { // for Windows 246 if (me.isPopupTrigger()) { 247 showPopUp(me); // display the popup 248 } 249 } 250 251 @Override 252 public void mousePressed(MouseEvent me) { // for macOS, Linux 253 if (me.isPopupTrigger()) { 254 log.debug("what's clicking?"); 255 showPopUp(me); // display the popup 256 } 257 } 258 }); 259 if (showToolTip) { 260 setToolTipText(switchTooltip); 261 } 262 if (iconSwitch != null) { 263 iconSwitch.setBackground(backgroundColor); 264 iconSwitch.setLabels(switchLabel, _uLabel); 265 } 266 // connect to object or dim switch 267 if (bean == null) { 268 if (!hideUnconnected) { 269 // to dim unconnected symbols TODO make graphics see through, now icons just become bleak 270 //float dim = 100f; 271 switch (_shape) { 272 case SwitchboardEditor.BUTTON: 273 beanButton.setEnabled(false); 274 break; 275 case SwitchboardEditor.SLIDER: 276 case SwitchboardEditor.KEY: 277 case SwitchboardEditor.SYMBOL: 278 default: 279 // iconSwitch.setOpacity(dim); // activate for graphic painted switches 280 } 281 displayState(0); // show unconnected as unknown/greyed 282 } 283 } else { 284 _control = true; 285 switch (beanTypeChar) { 286 case 'T': 287 getTurnout().addPropertyChangeListener(this, _switchSysName, "Switchboard Editor Turnout Switch"); 288 if (getTurnout().canInvert()) { 289 this.setInverted(getTurnout().getInverted()); // only add and set when supported by object/connection 290 } 291 break; 292 case 'S': 293 getSensor().addPropertyChangeListener(this, _switchSysName, "Switchboard Editor Sensor Switch"); 294 if (getSensor().canInvert()) { 295 this.setInverted(getSensor().getInverted()); // only add and set when supported by object/connection 296 } 297 break; 298 default: // light 299 getLight().addPropertyChangeListener(this, _switchSysName, "Switchboard Editor Light Switch"); 300 // Lights do not support Invert 301 } 302 displayState(bean.getState()); 303 } 304 log.debug("Created switch {}", index); 305 } 306 307 static final AffineTransform affinetransform = new AffineTransform(); 308 static final FontRenderContext frc = new FontRenderContext(affinetransform,true,true); 309 310 int getLabelFontSize(int radius, String text) { 311 int fontSize = Math.max(12, radius/4); 312 // see if that fits using font metrics 313 if (text != null) { 314 Font font = new Font(Font.SANS_SERIF, Font.BOLD, fontSize); 315 int textwidth = (int)(font.getStringBounds(text, frc).getWidth()); 316 fontSize = Math.min(fontSize, fontSize*2*radius*9/textwidth/10); // integer arithmetic: fit in 90% of radius*2 317 log.trace("calculate fontsize {} from radius {} and textwidth {} for string \"{}\"", fontSize, radius, textwidth, text); 318 } 319 return Math.max(fontSize, 5); // but go no smaller than 6 point 320 } 321 322 int getSubLabelFontSize(int radius, String text) { 323 int fontSize = Math.max(9, radius/5); 324 // see if text fits using font metrics, if not correct it with a smaller font size 325 if (text != null) { 326 Font font = new Font(Font.SANS_SERIF, Font.BOLD, fontSize); 327 int textwidth = (int)(font.getStringBounds(text, frc).getWidth()); 328 fontSize = Math.min(fontSize, fontSize*2*radius*9/textwidth/10); // integer arithmetic: fit in 90% of radius*2 329 log.trace("calculate fontsize {} from radius {} and textwidth {} for string \"{}\"", fontSize, radius, textwidth, text); 330 } 331 return Math.max(fontSize, 5); // but go no smaller than 6 point 332 } 333 334 public NamedBean getNamedBean() { 335 return _bname; 336 } 337 338 /** 339 * Store an object as NamedBeanHandle, using _label as the display 340 * name. 341 * 342 * @param bean the object (either a Turnout, Sensor or Light) to attach 343 * to this switch 344 */ 345 public void setNamedBean(@Nonnull NamedBean bean) { 346 try { 347 namedBean = nbhm.getNamedBeanHandle(_switchSysName, bean); 348 } catch (IllegalArgumentException e) { 349 log.error("invalid bean name= \"{}\" in Switchboard Button \"{}\"", _switchSysName, _switchDisplayName); 350 } 351 _uName = bean.getUserName(); 352 if (_uName == null) { 353 _uName = Bundle.getMessage("NoUserName"); 354 } else { 355 if (showUserName == SwitchBoardLabelDisplays.BOTH_NAMES) { 356 _uLabel = _uName; 357 } else if (showUserName == SwitchBoardLabelDisplays.USER_NAME) { 358 switchLabel = _uName; 359 } 360 } 361 _control = true; 362 } 363 364 public Turnout getTurnout() { 365 if (namedBean == null) { 366 return null; 367 } 368 return (Turnout) namedBean.getBean(); 369 } 370 371 public Sensor getSensor() { 372 if (namedBean == null) { 373 return null; 374 } 375 return (Sensor) namedBean.getBean(); 376 } 377 378 public Light getLight() { 379 if (namedBean == null) { 380 return null; 381 } 382 return (Light) namedBean.getBean(); 383 } 384 385 /** 386 * Get the user selected switch shape (e.g. 3 for Slider) 387 * 388 * @return the index of the selected item in Shape comboBox 389 */ 390 public int getShape() { 391 return _shape; 392 } 393 394 /** 395 * Get text to display on this switch on Switchboard and in Web Server panel when attached 396 * object is Active. 397 * 398 * @return text to show on active state (differs per type of object) 399 */ 400 public String getActiveText() { 401 // fetch bean specific abbreviation 402 if (beanTypeChar == 'T') { 403 _stateSign = stateClosed; // + 404 } else { 405 // Light, Sensor 406 _stateSign = "+"; // 1 char abbreviation for StateOff not clear 407 } 408 return _switchDisplayName + ": " + _stateSign; 409 } 410 411 /** 412 * Get text to display on this switch on Switchboard and in Web Server panel when attached 413 * object is Inactive. 414 * 415 * @return text to show on inactive state (differs per type of objects) 416 */ 417 public String getInactiveText() { 418 // fetch bean specific abbreviation 419 if (beanTypeChar == 'T') { 420 _stateSign = stateThrown; // + 421 } else { 422 // Light, Sensor 423 _stateSign = "-"; // 1 char abbreviation for StateOff not clear 424 } 425 return _switchDisplayName + ": " + _stateSign; 426 } 427 428 /** 429 * Get text to display on this switch in Web Server panel when attached 430 * object is Unknown (initial state displayed). 431 * 432 * @return text to show on unknown state (used on all types of objects) 433 */ 434 public String getUnknownText() { 435 return _switchDisplayName + ": ?"; 436 } 437 438 public String getInconsistentText() { 439 return _switchDisplayName + ": X"; 440 } 441 442 /** 443 * Get text to display as switch tooltip in Web Server panel. 444 * Used in jmri.jmrit.display.switchboardEditor.configureXml.BeanSwitchXml#store(Object) 445 * 446 * @return switch tooltip text 447 */ 448 public String getToolTip() { 449 return switchTooltip; 450 } 451 452 // ******************* Display *************************** 453 454 @Override 455 public void actionPerformed(ActionEvent e) { 456 //updateBean(); 457 } 458 459 /** 460 * Get the label of this switch. 461 * 462 * @return display name not including current state 463 */ 464 public String getNameString() { 465 return _switchDisplayName; 466 } 467 468 public String getUserNameString() { 469 return _uLabel; 470 } 471 472 private String getSwitchButtonLabel(String label) { 473 if ((showUserName == SwitchBoardLabelDisplays.SYSTEM_NAME) || (_uLabel.equals(""))) { 474 String subLabel = label.substring(0, (Math.min(label.length(), 35))); // reasonable max. to display 2 lines on tile 475 return "<html><center>" + subLabel + "</center></html>"; // lines of text 476 } else if (showUserName == SwitchBoardLabelDisplays.USER_NAME) { 477 String subLabel = label.substring(0, (Math.min(label.length(), 35))); // reasonable max. to display 2 lines on tile 478 return "<html><center>" + subLabel + "</center></html>"; // lines of text 479 } else { // BOTH_NAMES case 480 String subLabel = _uLabel.substring(0, (Math.min(_uLabel.length(), 35))); // reasonable max. to display 2 lines on tile 481 return "<html><center>" + label + "</center><center><i>" + subLabel + "</i></center></html>"; // lines of text 482 } 483 } 484 485 private String getSwitchButtonToolTip(String label) { 486 if ((showUserName == SwitchBoardLabelDisplays.SYSTEM_NAME) || (_uLabel.equals(""))) { 487 return label; 488 } else if (showUserName == SwitchBoardLabelDisplays.USER_NAME) { 489 return label; 490 } else { // BOTH_NAMES case 491 return _uLabel+" "+label; 492 } 493 } 494 495 496 /** 497 * Drive the current state of the display from the state of the 498 * connected bean. 499 * 500 * @param newState integer representing the new state e.g. Turnout.CLOSED 501 */ 502 public void displayState(int newState) { 503 String switchLabel; 504 Color switchColor; 505 if (getNamedBean() == null) { 506 switchLabel = _switchDisplayName; // unconnected, doesn't show state using : and ? 507 switchColor = Color.GRAY; 508 log.debug("Switch label {} state {}, disconnected", switchLabel, newState); 509 } else { 510 if (newState == _showingState) { 511 return; // prevent redrawing on repeated identical commands 512 } 513 // display abbreviated name of state instead of state index, fine for unconnected switches too 514 switch (newState) { 515 case 1: 516 switchLabel = getUnknownText(); 517 switchColor = Color.GRAY; 518 break; 519 case 2: 520 switchLabel = getActiveText(); 521 switchColor = activeColor; 522 break; 523 case 4: 524 switchLabel = getInactiveText(); 525 switchColor = inactiveColor; 526 break; 527 default: 528 switchLabel = getInconsistentText(); 529 switchColor = Color.WHITE; 530 //log.warn("SwitchState INCONSISTENT"); // normal for unconnected switchboard 531 log.debug("Switch label {} state: {}, connected", switchLabel, newState); 532 } 533 } 534 if (isText() && !isIcon()) { // to allow text buttons on web switchboard. 535 log.debug("Label = {}, setText", getSwitchButtonLabel(switchLabel)); 536 beanButton.setText(getSwitchButtonLabel(switchLabel)); 537 beanButton.setToolTipText(getSwitchButtonToolTip(switchLabel)); 538 beanButton.setBackground(switchColor); // only the color is visible on macOS 539 // TODO get access to bg color of JButton? 540 beanButton.setOpaque(true); 541 } else if (isIcon() && (iconSwitch != null)) { 542 iconSwitch.showSwitchIcon(newState); 543 iconSwitch.setLabels(switchLabel, _uLabel); 544 } 545 _showingState = newState; 546 } 547 548 /** 549 * Switch presentation is graphic image based. 550 * 551 * @see #displayState(int) 552 * @return true when switch shape other than 'Button' is selected 553 */ 554 public final boolean isIcon() { 555 return _icon; 556 } 557 558 /** 559 * Switch presentation is text based. 560 * 561 * @see #displayState(int) 562 * @return true when switch shape 'Button' is selected (and also for the 563 * other, graphic switch types until SwitchboardServlet directly 564 * supports their graphic icons) 565 */ 566 public final boolean isText() { 567 return _text; 568 } 569 570 /** 571 * Update switch as state of bean changes. 572 * 573 * @param e the PropertyChangeEvent heard 574 */ 575 @Override 576 public void propertyChange(java.beans.PropertyChangeEvent e) { 577 if (log.isDebugEnabled()) { 578 log.debug("property change: {} {} is now: {}", _switchSysName, e.getPropertyName(), e.getNewValue()); 579 } 580 if ( NamedBean.PROPERTY_KNOWN_STATE.equals(e.getPropertyName())) { 581 int now = ((Integer) e.getNewValue()); 582 displayState(now); 583 log.debug("Item state changed"); 584 } 585 if ( NamedBean.PROPERTY_USERNAME.equals(e.getPropertyName())) { 586 // update tooltip 587 String newUserName; 588 if (showToolTip) { 589 newUserName = ((String) e.getNewValue()); 590 _uLabel = (newUserName == null ? "" : newUserName); // store for display on icon 591 if (newUserName == null || newUserName.isEmpty()) { 592 newUserName = Bundle.getMessage("NoUserName"); // longer for tooltip 593 } 594 setToolTipText(_switchSysName + " (" + newUserName + ")"); 595 log.debug("User Name changed to {}", newUserName); 596 } 597 } 598 } 599 600 void cleanup() { 601 if (namedBean != null) { 602 switch (beanTypeChar) { 603 case 'T': 604 getTurnout().removePropertyChangeListener(this); 605 break; 606 case 'S': 607 getSensor().removePropertyChangeListener(this); 608 break; 609 default: // light 610 getLight().removePropertyChangeListener(this); 611 } 612 } 613 namedBean = null; 614 } 615 616 JPopupMenu switchPopup; 617 JMenuItem connectNewMenu = new JMenuItem(Bundle.getMessage("ConnectNewMenu", "...")); 618 619 /** 620 * Show pop-up on a switch with its unique attributes including the 621 * (un)connected bean. 622 * 623 * @param e unused because we now our own location 624 * @return true when pop up displayed 625 */ 626 public boolean showPopUp(MouseEvent e) { 627 if (switchPopup != null) { 628 switchPopup.removeAll(); 629 } else { 630 switchPopup = new JPopupMenu(); 631 } 632 633 switchPopup.add(getNameString()); 634 635 if (panelEditable && allControlling) { 636 if (namedBean != null) { 637 addEditUserName(switchPopup); 638 switch (beanTypeChar) { 639 case 'T': 640 if (getTurnout().canInvert()) { // check whether supported by this turnout 641 addInvert(switchPopup); 642 } 643 // tristate and momentary (see TurnoutIcon) can't be set per switch 644 break; 645 case 'S': 646 if (getSensor().canInvert()) { // check whether supported by this sensor 647 addInvert(switchPopup); 648 } 649 break; 650 default: 651 // invert is not supported by Lights, so skip 652 } 653 } else { 654 // show option to attach a new bean 655 switchPopup.add(connectNewMenu); 656 connectNewMenu.addActionListener((java.awt.event.ActionEvent e1) -> connectNew()); 657 } 658 } 659 // display the popup 660 switchPopup.show(this, this.getWidth()/3 + (int) ((popScale - 1.0) * this.getX()), 661 this.getHeight()/3 + (int) ((popScale - 1.0) * this.getY())); 662 663 return true; 664 } 665 666 javax.swing.JMenuItem editItem = null; 667 668 void addEditUserName(JPopupMenu popup) { 669 editItem = new javax.swing.JMenuItem(Bundle.getMessage("EditNameTitle", "...")); 670 popup.add(editItem); 671 editItem.addActionListener((java.awt.event.ActionEvent e) -> renameBeanDialog()); 672 } 673 674 javax.swing.JCheckBoxMenuItem invertItem = null; 675 676 void addInvert(JPopupMenu popup) { 677 invertItem = new javax.swing.JCheckBoxMenuItem(Bundle.getMessage("MenuInvertItem", _switchSysName)); 678 invertItem.setSelected(getInverted()); 679 popup.add(invertItem); 680 invertItem.addActionListener((java.awt.event.ActionEvent e) -> setBeanInverted(invertItem.isSelected())); 681 } 682 683 /** 684 * Edit user name on a switch. 685 */ 686 public void renameBeanDialog() { 687 String oldName = _uName; 688 String newUserName = (String)JmriJOptionPane.showInputDialog(this, Bundle.getMessage("EnterNewName", _switchSysName), Bundle.getMessage("EditNameTitle", ""), 689 JmriJOptionPane.QUESTION_MESSAGE, null, null, oldName); 690 691 if (newUserName == null) { 692 return; 693 } 694 if (newUserName.equals(Bundle.getMessage("NoUserName")) || newUserName.isEmpty()) { // user cancelled 695 log.debug("new user name was empty"); 696 JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("WarningEmptyUserName"), Bundle.getMessage("WarningTitle"), JmriJOptionPane.ERROR_MESSAGE); 697 return; 698 } 699 renameBean(newUserName, oldName); 700 } 701 702 /** 703 * Edit user name on a switch. 704 * 705 * @param newUserName string to use as user name replacement 706 * @param oldName current user name (used to prevent useless change) 707 */ 708 protected void renameBean(String newUserName, String oldName) { 709 NamedBean nb; 710 if (newUserName.equals(oldName)) { // name was not changed by user 711 return; 712 } else { // check if name is already in use 713 switch (beanTypeChar) { 714 case 'T': 715 nb = jmri.InstanceManager.turnoutManagerInstance().getTurnout(newUserName); 716 break; 717 case 'S': 718 nb = jmri.InstanceManager.sensorManagerInstance().getSensor(newUserName); 719 break; 720 case 'L': 721 nb = jmri.InstanceManager.lightManagerInstance().getLight(newUserName); 722 break; 723 default: 724 log.error("Check userName: cannot parse bean name. userName = {}", newUserName); 725 return; 726 } 727 if (nb != null) { 728 log.error("User name is not unique {}", newUserName); 729 String msg = Bundle.getMessage("WarningUserName", newUserName); 730 JmriJOptionPane.showMessageDialog(this, msg, 731 Bundle.getMessage("WarningTitle"), 732 JmriJOptionPane.ERROR_MESSAGE); 733 return; 734 } 735 } 736 _bname.setUserName(newUserName); 737 if (oldName == null || oldName.equals("")) { 738 if (!nbhm.inUse(_switchSysName, _bname)) { 739 return; // no problem, so stop 740 } 741 String msg = Bundle.getMessage("UpdateToUserName", switchTypeName, newUserName, _switchSysName); 742 int optionPane = JmriJOptionPane.showConfirmDialog(this, 743 msg, Bundle.getMessage("UpdateToUserNameTitle"), 744 JmriJOptionPane.YES_NO_OPTION); 745 if (optionPane == JmriJOptionPane.YES_OPTION) { 746 //This will update the bean reference from the systemName to the userName 747 try { 748 nbhm.updateBeanFromSystemToUser(_bname); 749 } catch (JmriException ex) { 750 // We should never get an exception here as we already check that the username is not valid 751 } 752 } 753 754 } else { 755 nbhm.renameBean(oldName, newUserName, _bname); // will pick up name change in label 756 } 757 if (_editor != null) { 758 _editor.updatePressed(); // but we redraw whole switchboard 759 } 760 } 761 762 private boolean inverted = false; 763 764 public void setInverted(boolean set) { 765 inverted = set; 766 } 767 768 public boolean getInverted() { 769 return inverted; 770 } 771 772 /** 773 * Invert attached object on the layout, if supported by its connection. 774 * 775 * @param set new inverted state, true for inverted, false for normal. 776 */ 777 public void setBeanInverted(boolean set) { 778 switch (beanTypeChar) { 779 case 'T': 780 if (getTurnout() != null && getTurnout().canInvert()) { // if supported 781 this.setInverted(set); 782 getTurnout().setInverted(set); 783 } 784 break; 785 case 'S': 786 if (getSensor() != null && getSensor().canInvert()) { // if supported 787 this.setInverted(set); 788 getSensor().setInverted(set); 789 } 790 break; 791 case 'L': 792 // Lights cannot be inverted, so never called 793 return; 794 default: 795 log.error("Invert item: cannot parse bean name. userName = {}", _switchSysName); 796 } 797 } 798 799 /** 800 * Process mouseClick on this switch, passing in name for debug. 801 * 802 * @param e the event heard 803 * @param name ID of this button (identical to name of suggested bean 804 * object) 805 */ 806 public void operate(MouseEvent e, String name) { 807 log.debug("Button {} clicked", name); 808 if (namedBean == null || e == null || e.isMetaDown()) { 809 return; 810 } 811 alternateOnClick(); 812 } 813 814 /** 815 * Process mouseClick on this switch. 816 * Similar to {@link #operate(MouseEvent, String)}. 817 * 818 * @param e the event heard 819 */ 820 public void doMouseClicked(java.awt.event.MouseEvent e) { 821 log.debug("Switch clicked"); 822 if (namedBean == null || e == null || e.isMetaDown()) { 823 return; 824 } 825 alternateOnClick(); 826 } 827 828 /** 829 * Change the state of attached Turnout, Light or Sensor on the layout 830 * unless menu option Panel Items Control Layout is set to off. 831 */ 832 void alternateOnClick() { 833 if (allControlling) { 834 switch (beanTypeChar) { 835 case 'T': // Turnout 836 log.debug("T clicked"); 837 if (getTurnout().getKnownState() == jmri.Turnout.CLOSED) // if clear known state, set to opposite 838 { 839 getTurnout().setCommandedState(jmri.Turnout.THROWN); 840 } else if (getTurnout().getKnownState() == jmri.Turnout.THROWN) { 841 getTurnout().setCommandedState(jmri.Turnout.CLOSED); 842 } else if (getTurnout().getCommandedState() == jmri.Turnout.CLOSED) { 843 getTurnout().setCommandedState(jmri.Turnout.THROWN); // otherwise, set to opposite of current commanded state if known 844 } else { 845 getTurnout().setCommandedState(jmri.Turnout.CLOSED); // just force Closed 846 } 847 break; 848 case 'L': // Light 849 log.debug("L clicked"); 850 if (getLight().getState() == jmri.Light.OFF) { 851 getLight().setState(jmri.Light.ON); 852 } else { 853 getLight().setState(jmri.Light.OFF); 854 } 855 break; 856 case 'S': // Sensor 857 log.debug("S clicked"); 858 try { 859 if (getSensor().getKnownState() == jmri.Sensor.INACTIVE) { 860 getSensor().setKnownState(jmri.Sensor.ACTIVE); 861 } else { 862 getSensor().setKnownState(jmri.Sensor.INACTIVE); 863 } 864 } catch (jmri.JmriException reason) { 865 log.warn("Exception flipping sensor", (Object) reason); 866 } 867 break; 868 default: 869 log.error("invalid char in Switchboard Button \"{}\". State not set.", _switchSysName); 870 } 871 } 872 } 873 874 /** 875 * Only for lights. Used for All Off/All On. 876 * Skips unconnected switch icons. 877 * 878 * @param state On = 1, Off = 0 879 */ 880 public void switchLight(int state) { 881 if (namedBean != null) { 882 getLight().setState(state); 883 } 884 } 885 886 public void setBackgroundColor(Color bgcolor) { 887 this.setBackground(bgcolor); 888 } 889 890 JmriJFrame addFrame = null; 891 JTextField sysNameTextBox = new JTextField(12); 892 JTextField userName = new JTextField(15); 893 894 /** 895 * Create new bean and connect it to this switch. Use type letter from 896 * switch label (T, S or L). 897 */ 898 protected void connectNew() { 899 log.debug("Request new bean"); 900 userName.setText(""); // this method is only available on unconnected switches, so no useful content to fill in yet 901 // provide etc. 902 if (addFrame == null) { 903 addFrame = new JmriJFrame(Bundle.getMessage("ConnectNewMenu", ""), false, true); 904 addFrame.addHelpMenu("package.jmri.jmrit.display.switchboardEditor.SwitchboardEditor", true); 905 addFrame.getContentPane().setLayout(new BoxLayout(addFrame.getContentPane(), BoxLayout.Y_AXIS)); 906 907 ActionListener okListener = this::okAddPressed; 908 ActionListener cancelListener = this::cancelAddPressed; 909 AddNewDevicePanel switchConnect = new AddNewDevicePanel(sysNameTextBox, userName, "ButtonOK", okListener, cancelListener); 910 switchConnect.setSystemNameFieldIneditable(); // prevent user interference with switch label (proposed system name) 911 switchConnect.setOK(); // activate OK button on Add new device pane 912 addFrame.add(switchConnect); 913 } 914 ThreadingUtil.runOnGUI( () -> { 915 addFrame.pack(); 916 addFrame.setLocationRelativeTo(this); 917 addFrame.setVisible(true); 918 }); 919 } 920 921 protected void cancelAddPressed(ActionEvent e) { 922 if (addFrame != null) { 923 ThreadingUtil.runOnGUI( () -> addFrame.setVisible(false) ); 924 addFrame.dispose(); 925 addFrame = null; 926 } 927 } 928 929 protected void okAddPressed(ActionEvent e) { 930 NamedBean nb; 931 String user = userName.getText(); 932 if (user.isBlank()) { 933 user = null; 934 } 935 // systemName can't be changed, fixed 936 if (addFrame != null) { 937 addFrame.setVisible(false); 938 addFrame.dispose(); 939 addFrame = null; 940 } 941 switch (_switchSysName.charAt(manuPrefix.length())) { 942 case 'T': 943 if ( existingUserName(InstanceManager.getDefault(TurnoutManager.class), user, e)) { 944 return; 945 } 946 Turnout t; 947 try { 948 // add turnout to JMRI (w/appropriate manager) 949 t = InstanceManager.turnoutManagerInstance().provideTurnout(_switchSysName); 950 t.setUserName(user); 951 } catch (IllegalArgumentException ex) { 952 // user input no good 953 handleCreateException(_switchSysName, ex); 954 return; // without creating 955 } 956 nb = jmri.InstanceManager.turnoutManagerInstance().getTurnout(_switchSysName); 957 break; 958 case 'S': 959 if ( existingUserName(InstanceManager.getDefault(SensorManager.class), user, e)) { 960 return; 961 } 962 Sensor s; 963 try { 964 // add Sensor to JMRI (w/appropriate manager) 965 s = InstanceManager.sensorManagerInstance().provideSensor(_switchSysName); 966 s.setUserName(user); 967 } catch (IllegalArgumentException ex) { 968 // user input no good 969 handleCreateException(_switchSysName, ex); 970 return; // without creating 971 } 972 nb = jmri.InstanceManager.sensorManagerInstance().getSensor(_switchSysName); 973 break; 974 case 'L': 975 if ( existingUserName(InstanceManager.getDefault(LightManager.class), user, e)) { 976 return; 977 } 978 Light l; 979 try { 980 // add Light to JMRI (w/appropriate manager) 981 l = InstanceManager.lightManagerInstance().provideLight(_switchSysName); 982 l.setUserName(user); 983 } catch (IllegalArgumentException ex) { 984 // user input no good 985 handleCreateException(_switchSysName, ex); 986 return; // without creating 987 } 988 nb = jmri.InstanceManager.lightManagerInstance().getLight(_switchSysName); 989 break; 990 default: 991 log.error("connectNew - okAddPressed: cannot parse bean name. sName = {}", _switchSysName); 992 return; 993 } 994 if (nb == null) { 995 log.warn("failed to connect switch to item {}", _switchSysName); 996 } else { 997 // set switch on Switchboard to display current state of just connected bean 998 log.debug("sName state: {}", nb.getState()); 999 try { 1000 if (_editor.getSwitch(_switchSysName) == null) { 1001 log.warn("failed to update switch to state of {}", _switchSysName); 1002 } else { 1003 _editor.updatePressed(); 1004 } 1005 } catch (NullPointerException npe) { 1006 handleCreateException(_switchSysName, npe); 1007 // exit without updating 1008 } 1009 } 1010 } 1011 1012 private boolean existingUserName(Manager<?> mgr, String userName, Object e){ 1013 NamedBean nb = mgr.getByUserName(userName == null ? "" : userName); 1014 if ( nb != null ) { 1015 JmriJOptionPane.showMessageDialog( JmriJOptionPane.findWindowForObject(e), 1016 Bundle.getMessage("InvalidUserNameAlreadyExists",nb.getBeanType(),userName), 1017 Bundle.getMessage("WarningUserName", userName), JmriJOptionPane.ERROR_MESSAGE); 1018 return true; 1019 } 1020 return false; 1021 } 1022 1023 /** 1024 * Check the switch label currently displayed. 1025 * Used in test. 1026 * 1027 * @return line 1 of the label of this switch 1028 */ 1029 protected String getIconLabel() { 1030 switch (_shape) { 1031 case SwitchboardEditor.BUTTON: // button 1032 String lbl = beanButton.getText(); 1033 if (!lbl.startsWith("<")) { 1034 return lbl; 1035 } else { // 2 line label, "<html><center>" + label + "</center>..." 1036 return lbl.substring(14, lbl.indexOf("</center>")); 1037 } 1038 case SwitchboardEditor.SLIDER: 1039 case SwitchboardEditor.KEY: 1040 case SwitchboardEditor.SYMBOL: 1041 return iconSwitch.getIconLabel(); 1042 default: 1043 return ""; 1044 } 1045 } 1046 1047 void handleCreateException(String sysName, Exception ex) { 1048 JmriJOptionPane.showMessageDialog(addFrame, 1049 "<html>" + Bundle.getMessage("ErrorSwitchAddFailed") + "<br>" + ex.getLocalizedMessage()+"</html>", 1050 Bundle.getMessage("ErrorTitle") + " : " + sysName, 1051 JmriJOptionPane.ERROR_MESSAGE); 1052 } 1053 1054 String rootPath = "resources/icons/misc/switchboard/"; 1055 1056 /** 1057 * Class to display individual bean state switches on a JMRI Switchboard 1058 * using 2DGraphic drawing code or alternating 2 image files. 1059 */ 1060 public class IconSwitch extends JPanel { 1061 1062 private BufferedImage image; 1063 private BufferedImage image1; 1064 private BufferedImage image2; 1065 private String tag = "tag"; 1066 private String subTag = ""; 1067 1068 private int labelX = 16; 1069 private int labelY = 53; 1070 private int textSize = 12; 1071 private float textAlign = 0.0f; 1072 1073 private int subLabelX = 16; 1074 private int subLabelY = 53; 1075 private int subTextSize = 12; 1076 private float subTextAlign = 0.0f; 1077 1078 private float ropOffset = 0f; 1079 private int r = 10; // radius of circle fitting inside tile rect in px drawing units 1080 private int _shape = SwitchboardEditor.BUTTON; 1081 private int _state = 0; 1082 private RescaleOp rop; 1083 1084 /** 1085 * Create an icon from 2 alternating png images. shape is assumed SwitchboardEditor.SYMBOL 1086 * 1087 * @param filepath1 the ON image 1088 * @param filepath2 the OFF image 1089 * @param back the background color set on the Switchboard, used to fill in empty parts of rescaled image 1090 */ 1091 public IconSwitch(String filepath1, String filepath2, Color back) { 1092 // load image files 1093 try { 1094 image1 = ImageIO.read(new File(filepath1)); 1095 image2 = ImageIO.read(new File(filepath2)); 1096 if ((square != 100) && (square >= 25) && (square <= 150)) { 1097 image1 = resizeImage(image1, square, back); 1098 image2 = resizeImage(image2, square, back); 1099 } 1100 image = image2; // start off as showing inactive/closed 1101 } catch (IOException ex) { 1102 log.error("error reading image from {}-{}", filepath1, filepath2, ex); 1103 } 1104 _shape = SwitchboardEditor.SYMBOL; 1105 if (radius > 10) r = radius; 1106 log.debug("radius={} size={}", r, getWidth()); 1107 } 1108 1109 /** 1110 * Ctor to draw graphic fully in Graphics. 1111 * 1112 * @param shape int to specify switch shape {@link SwitchboardEditor} constants 1113 * @param type beanType to draw (optionally ignored depending on shape, eg. for slider) 1114 */ 1115 public IconSwitch(int shape, int type) { 1116 if ((shape == SwitchboardEditor.BUTTON) || (shape == SwitchboardEditor.SYMBOL)) { 1117 return; // when SYMBOL is migrated, leave in place for 0 = BUTTON (drawn as JButtons, not graphics) 1118 } 1119 _shape = shape; 1120 if (radius > 10) r = radius; 1121 log.debug("DrawnIcon type={}", type); 1122 } 1123 1124 public void setOpacity(float offset) { 1125 ropOffset = offset; 1126 float ropScale = 1.0f; 1127 rop = new RescaleOp(ropScale, ropOffset, null); 1128 } 1129 1130 protected void showSwitchIcon(int stateIndex) { 1131 log.debug("showSwitchIcon {}", stateIndex); 1132 if ((_shape == SwitchboardEditor.SLIDER) || (_shape == SwitchboardEditor.KEY)) { 1133 //redraw (colors are already set above 1134 _state = stateIndex; 1135 } else { 1136 if (image1 != null && image2 != null) { 1137 switch (stateIndex) { 1138 case 2: 1139 image = image1; // on/Thrown/Active 1140 break; 1141 case 1: 1142 default: 1143 image = image2; // off, also for connected & unknown 1144 break; 1145 } 1146 this.repaint(); 1147 } 1148 } 1149 } 1150 1151 /** 1152 * Set or change label text on switch. 1153 * 1154 * @param sName string to display (system name) 1155 * @param uName secondary string to display (user name) 1156 */ 1157 protected void setLabels(String sName, String uName) { 1158 tag = sName; 1159 subTag = uName; 1160 this.repaint(); 1161 } 1162 1163 private String getIconLabel() { 1164 return tag; 1165 } 1166 1167 /** 1168 * Position (sub)label on switch. 1169 * 1170 * @param x horizontal offset from top left corner, positive to the 1171 * right 1172 * @param y vertical offset from top left corner, positive down 1173 * @param align one of: JComponent.LEFT_ALIGNMENT (0.0f), CENTER_ALIGNMENT (0.5f), 1174 * RIGHT_ALIGNMENT (1.0f) 1175 * @param fontsize size in points for label text display 1176 */ 1177 protected void positionLabel(int x, int y, float align, int fontsize) { 1178 labelX = x; 1179 labelY = y; 1180 textAlign = align; 1181 textSize = fontsize; 1182 } 1183 1184 protected void positionSubLabel(int x, int y, float align, int fontsize) { 1185 subLabelX = x; 1186 subLabelY = y; 1187 subTextAlign = align; 1188 subTextSize = fontsize; 1189 } 1190 1191 @Override 1192 protected void paintComponent(Graphics g) { 1193 super.paintComponent(g); 1194 Graphics2D g2d = (Graphics2D) g; 1195 // set antialiasing hint for macOS and Windows 1196 // note: antialiasing has performance problems on some variants of Linux (Raspberry pi) 1197 if (SystemType.isMacOSX() || SystemType.isWindows()) { 1198 g2d.setRenderingHint(RenderingHints.KEY_RENDERING, 1199 RenderingHints.VALUE_RENDER_QUALITY); 1200 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 1201 RenderingHints.VALUE_ANTIALIAS_ON); 1202 g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, 1203 RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); 1204 g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, 1205 RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 1206 } 1207 // now the image 1208 g.translate(r, r); // set origin to center 1209 if (_shape == SwitchboardEditor.SLIDER) { // slider 1210 // Draw symbol on the beanswitch widget canvas 1211 // see panel.js for vector drawing: var $drawWidgetSymbol = function(id, state), ctx is same as g2d 1212 // clear for alternating text and 'moving' items not covered by new paint 1213 if (_state == 4) { 1214 g.setColor(inactiveColor); // simple change in color 1215 } else if (_state == 2) { 1216 g.setColor(activeColor); 1217 } else { 1218 g.setColor(Color.GRAY); 1219 } 1220 // slider, same shape for all beanTypes (S, T, L) 1221 // the sliderspace 1222 g2d.fillRoundRect(-r/2, 0, r, r/2, r/2, r/2); 1223 g.setColor((_state == 2 || _state == 4) ? Color.BLACK : Color.GRAY); 1224 g2d.drawRoundRect(-r/2, 0, r, r/2, r/2, r/2); 1225 // the knob 1226 int knobX = (_state == 2 ? 0 : -r/2); 1227 g.setColor(Color.WHITE); 1228 g2d.fillOval(knobX, 0, r/2, r/2); 1229 g.setColor(Color.BLACK); 1230 g2d.drawOval(knobX, 0, r/2, r/2); 1231 //g2d.drawRect(-r, -r, 2*r, 2*r); // debug tile size outline 1232 } else if (_shape == SwitchboardEditor.KEY) { 1233 // key, same shape for all beanTypes (S, T, L) 1234 // red = upper rounded rect 1235 g.setColor(_state == 2 ? activeColor : SwitchboardEditor.darkActiveColor); // simple change in color 1236 g2d.fillRoundRect(-3*r/8, -2*r/3, 3*r/4, r/3, r/6, r/6); 1237 // green = lower rounded rect 1238 g.setColor(_state == 4 ? inactiveColor : SwitchboardEditor.darkInactiveColor); // simple change in color 1239 g2d.fillRoundRect(-3*r/8, r/3, 3*r/4, r/3, r/6, r/6); 1240 // add round LED at top (only part defined as floats) 1241 Point2D center = new Point2D.Float(0.05f*r, -7.0f*r/8.0f); 1242 float radius = r/6.0f; 1243 float[] dist = {0.0f, 0.8f}; 1244 Color[] colors = {Color.WHITE, (_state == 2 ? activeColor : Color.GRAY)}; 1245 RadialGradientPaint pnt = new RadialGradientPaint(center, radius, dist, colors); 1246 g2d.setPaint(pnt); 1247 g2d.fillOval(-r/8, -r, r/4, r/4); 1248 // with black outline 1249 g.setColor(Color.BLACK); 1250 g2d.drawOval(-r/8, -r, r/4, r/4); 1251 //g2d.drawRect(-r, -r, 2*r, 2*r); // debug tile size outline 1252 } else { 1253 // use image file 1254 g2d.drawImage(image, rop, image.getWidth()/-2, image.getHeight()/-2); // center bitmap 1255 //g2d.drawRect(-r, -r, 2*r, 2*r); // debug tile size outline 1256 } 1257 g.setFont(getFont()); 1258 if (ropOffset > 0f) { 1259 g.setColor(Color.GRAY); // dimmed 1260 } else { 1261 g.setColor(textColor); 1262 } 1263 1264 g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, textSize)); 1265 1266 if (Math.abs(textAlign - Component.CENTER_ALIGNMENT) < .0001) { 1267 FontMetrics metrics = g.getFontMetrics(); // figure out where the center of the string is 1268 labelX = metrics.stringWidth(tag)/-2; 1269 } 1270 g.drawString(tag, labelX, labelY); // draw name on top of button image (vertical, horizontal offset from top left) 1271 1272 if (showUserName == SwitchBoardLabelDisplays.BOTH_NAMES) { 1273 g.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, subTextSize)); 1274 if (Math.abs(subTextAlign - Component.CENTER_ALIGNMENT) < .0001) { 1275 FontMetrics metrics = g.getFontMetrics(); // figure out where the center of the string is 1276 subLabelX = metrics.stringWidth(subTag)/-2; 1277 } 1278 g.drawString(subTag, subLabelX, subLabelY); // draw user name at bottom 1279 } else { 1280 } 1281 } 1282 } 1283 1284 private void redispatchToParent(MouseEvent e){ 1285 Component source = (Component) e.getSource(); 1286 MouseEvent parentEvent = SwingUtilities.convertMouseEvent(source, e, source.getParent()); 1287 source.getParent().dispatchEvent(parentEvent); 1288 } 1289 1290 /** 1291 * Get a resized copy of the image. 1292 * 1293 * @param image the image to rescale 1294 * @param scale scale percentage as int (will be divided by 100 in operation) 1295 * @param background background color to paint on resized image, prevents null value (black) 1296 * @return a reduced/enlarged pixel image 1297 */ 1298 public static BufferedImage resizeImage(final Image image, int scale, Color background) { 1299 int newWidth = scale*(image.getWidth(null))/100; 1300 int newHeight = scale*image.getHeight(null)/100; 1301 final BufferedImage bimg = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); 1302 final Graphics2D g2d = bimg.createGraphics(); 1303 g2d.setColor(background); 1304 log.debug("BGCOLOR={}", background); 1305 g2d.fillRect(0, 0, newWidth, newHeight); 1306 //below three lines are for RenderingHints for better image quality at cost of higher processing time 1307 g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); 1308 g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); 1309 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 1310 g2d.drawImage(image, 0, 0, newWidth, newHeight, null); 1311 g2d.dispose(); 1312 return bimg; 1313 } 1314 1315 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BeanSwitch.class); 1316 1317}