001package jmri.jmrit.display; 002 003import java.awt.Color; 004import java.awt.Container; 005import java.awt.Dimension; 006import java.awt.Graphics; 007import java.awt.Graphics2D; 008import java.awt.RenderingHints; 009import java.awt.event.ActionEvent; 010import java.awt.event.ActionListener; 011import java.awt.geom.AffineTransform; 012import java.awt.geom.Point2D; 013import java.awt.image.BufferedImage; 014import java.beans.PropertyVetoException; 015import java.util.Objects; 016import java.util.HashSet; 017import java.util.Set; 018 019import javax.annotation.CheckForNull; 020import javax.annotation.Nonnull; 021import javax.swing.AbstractAction; 022import javax.swing.JCheckBoxMenuItem; 023import javax.swing.JComponent; 024import javax.swing.JFrame; 025import javax.swing.JLabel; 026import javax.swing.JPopupMenu; 027import javax.swing.JScrollPane; 028 029import jmri.InstanceManager; 030import jmri.jmrit.catalog.NamedIcon; 031import jmri.jmrit.display.palette.IconItemPanel; 032import jmri.jmrit.display.palette.ItemPanel; 033import jmri.jmrit.display.palette.TextItemPanel; 034import jmri.jmrit.logixng.*; 035import jmri.jmrit.logixng.tools.swing.DeleteBean; 036import jmri.util.MathUtil; 037import jmri.util.SystemType; 038import jmri.util.ThreadingUtil; 039import jmri.util.swing.JmriMouseEvent; 040 041import org.slf4j.Logger; 042import org.slf4j.LoggerFactory; 043 044/** 045 * PositionableLabel is a JLabel that can be dragged around the inside of the 046 * enclosing Container using a right-drag. 047 * <p> 048 * The positionable parameter is a global, set from outside. The 'fixed' 049 * parameter is local, set from the popup here. 050 * 051 * <a href="doc-files/Heirarchy.png"><img src="doc-files/Heirarchy.png" alt="UML class diagram for package" height="33%" width="33%"></a> 052 * @author Bob Jacobsen Copyright (c) 2002 053 */ 054public class PositionableLabel extends JLabel implements Positionable { 055 056 protected Editor _editor; 057 058 private String _id; // user's Id or null if no Id 059 private final Set<String> _classes = new HashSet<>(); // user's classes 060 061 protected boolean _icon = false; 062 protected boolean _text = false; 063 protected boolean _control = false; 064 protected NamedIcon _namedIcon; 065 066 protected ToolTip _tooltip; 067 protected boolean _showTooltip = true; 068 protected boolean _editable = true; 069 protected boolean _positionable = true; 070 protected boolean _viewCoordinates = true; 071 protected boolean _controlling = true; 072 protected boolean _hidden = false; 073 protected boolean _emptyHidden = false; 074 protected int _displayLevel; 075 076 protected String _unRotatedText; 077 protected boolean _rotateText = false; 078 private int _degrees; 079 080 private LogixNG _logixNG; 081 private String _logixNG_SystemName; 082 083 /** 084 * Create a new Positionable Label. 085 * @param s label string. 086 * @param editor where this label is displayed. 087 */ 088 public PositionableLabel(String s, @Nonnull Editor editor) { 089 super(s); 090 _editor = editor; 091 _text = true; 092 _unRotatedText = s; 093 log.debug("PositionableLabel ctor (text) {}", s); 094 setHorizontalAlignment(JLabel.CENTER); 095 setVerticalAlignment(JLabel.CENTER); 096 setPopupUtility(new PositionablePopupUtil(this, this)); 097 } 098 099 public PositionableLabel(@CheckForNull NamedIcon s, @Nonnull Editor editor) { 100 super(s); 101 _editor = editor; 102 _icon = true; 103 _namedIcon = s; 104 log.debug("PositionableLabel ctor (icon) {}", s != null ? s.getName() : null); 105 setPopupUtility(new PositionablePopupUtil(this, this)); 106 } 107 108 /** {@inheritDoc} */ 109 @Override 110 public void setId(String id) throws Positionable.DuplicateIdException { 111 if (Objects.equals(this._id, id)) return; 112 _editor.positionalIdChange(this, id); 113 this._id = id; 114 } 115 116 /** {@inheritDoc} */ 117 @Override 118 public String getId() { 119 return _id; 120 } 121 122 /** {@inheritDoc} */ 123 @Override 124 public void addClass(String className) { 125 _editor.positionalAddClass(this, className); 126 _classes.add(className); 127 } 128 129 /** {@inheritDoc} */ 130 @Override 131 public void removeClass(String className) { 132 _editor.positionalRemoveClass(this, className); 133 _classes.remove(className); 134 } 135 136 /** {@inheritDoc} */ 137 @Override 138 public void removeAllClasses() { 139 for (String className : _classes) { 140 _editor.positionalRemoveClass(this, className); 141 } 142 _classes.clear(); 143 } 144 145 /** {@inheritDoc} */ 146 @Override 147 public Set<String> getClasses() { 148 return java.util.Collections.unmodifiableSet(_classes); 149 } 150 151 public final boolean isIcon() { 152 return _icon; 153 } 154 155 public final boolean isText() { 156 return _text; 157 } 158 159 public final boolean isControl() { 160 return _control; 161 } 162 163 @Override 164 public @Nonnull Editor getEditor() { 165 return _editor; 166 } 167 168 @Override 169 public void setEditor(@Nonnull Editor ed) { 170 _editor = ed; 171 } 172 173 // *************** Positionable methods ********************* 174 @Override 175 public void setPositionable(boolean enabled) { 176 _positionable = enabled; 177 } 178 179 @Override 180 public final boolean isPositionable() { 181 return _positionable; 182 } 183 184 @Override 185 public void setEditable(boolean enabled) { 186 _editable = enabled; 187 showHidden(); 188 } 189 190 @Override 191 public boolean isEditable() { 192 return _editable; 193 } 194 195 @Override 196 public void setViewCoordinates(boolean enabled) { 197 _viewCoordinates = enabled; 198 } 199 200 @Override 201 public boolean getViewCoordinates() { 202 return _viewCoordinates; 203 } 204 205 @Override 206 public void setControlling(boolean enabled) { 207 _controlling = enabled; 208 } 209 210 @Override 211 public boolean isControlling() { 212 return _controlling; 213 } 214 215 @Override 216 public void setHidden(boolean hide) { 217 if (_hidden != hide) { 218 _hidden = hide; 219 showHidden(); 220 } 221 } 222 223 @Override 224 public boolean isHidden() { 225 return _hidden; 226 } 227 228 @Override 229 public void showHidden() { 230 if (!_hidden || _editor.isEditable()) { 231 setVisible(true); 232 } else { 233 setVisible(false); 234 } 235 } 236 237 @Override 238 public void setEmptyHidden(boolean hide) { 239 _emptyHidden = hide; 240 } 241 242 @Override 243 public boolean isEmptyHidden() { 244 return _emptyHidden; 245 } 246 247 /** 248 * Delayed setDisplayLevel for DnD. 249 * 250 * @param l the level to set 251 */ 252 public void setLevel(int l) { 253 _displayLevel = l; 254 } 255 256 @Override 257 public void setDisplayLevel(int l) { 258 int oldDisplayLevel = _displayLevel; 259 _displayLevel = l; 260 if (oldDisplayLevel != l) { 261 log.debug("Changing label display level from {} to {}", oldDisplayLevel, _displayLevel); 262 _editor.displayLevelChange(this); 263 } 264 } 265 266 @Override 267 public int getDisplayLevel() { 268 return _displayLevel; 269 } 270 271 @Override 272 public void setShowToolTip(boolean set) { 273 _showTooltip = set; 274 } 275 276 @Override 277 public boolean showToolTip() { 278 return _showTooltip; 279 } 280 281 @Override 282 public void setToolTip(ToolTip tip) { 283 _tooltip = tip; 284 } 285 286 @Override 287 public ToolTip getToolTip() { 288 return _tooltip; 289 } 290 291 @Override 292 @Nonnull 293 public String getTypeString() { 294 return Bundle.getMessage("PositionableType_PositionableLabel"); 295 } 296 297 @Override 298 @Nonnull 299 public String getNameString() { 300 if (_icon && _displayLevel > Editor.BKG) { 301 return "Icon"; 302 } else if (_text) { 303 return "Text Label"; 304 } else { 305 return "Background"; 306 } 307 } 308 309 /** 310 * When text is rotated or in an icon mode, the return of getText() may be 311 * null or some other value 312 * 313 * @return original defining text set by user 314 */ 315 public String getUnRotatedText() { 316 return _unRotatedText; 317 } 318 319 public void setUnRotatedText(String s) { 320 _unRotatedText = s; 321 } 322 323 @Override 324 @Nonnull 325 public Positionable deepClone() { 326 PositionableLabel pos; 327 if (_icon) { 328 NamedIcon icon = new NamedIcon((NamedIcon) getIcon()); 329 pos = new PositionableLabel(icon, _editor); 330 } else { 331 pos = new PositionableLabel(getText(), _editor); 332 } 333 return finishClone(pos); 334 } 335 336 protected @Nonnull Positionable finishClone(@Nonnull PositionableLabel pos) { 337 pos._text = _text; 338 pos._icon = _icon; 339 pos._control = _control; 340// pos._rotateText = _rotateText; 341 pos._unRotatedText = _unRotatedText; 342 pos.setLocation(getX(), getY()); 343 pos._displayLevel = _displayLevel; 344 pos._controlling = _controlling; 345 pos._hidden = _hidden; 346 pos._positionable = _positionable; 347 pos._showTooltip = _showTooltip; 348 pos.setToolTip(getToolTip()); 349 pos._editable = _editable; 350 if (getPopupUtility() == null) { 351 pos.setPopupUtility(null); 352 } else { 353 pos.setPopupUtility(getPopupUtility().clone(pos, pos.getTextComponent())); 354 } 355 pos.setOpaque(isOpaque()); 356 if (_namedIcon != null) { 357 pos._namedIcon = cloneIcon(_namedIcon, pos); 358 pos.setIcon(pos._namedIcon); 359 pos.rotate(_degrees); //this will change text in icon with a new _namedIcon. 360 } 361 pos.updateSize(); 362 return pos; 363 } 364 365 @Override 366 public @Nonnull JComponent getTextComponent() { 367 return this; 368 } 369 370 public static @Nonnull NamedIcon cloneIcon(NamedIcon icon, PositionableLabel pos) { 371 if (icon.getURL() != null) { 372 return new NamedIcon(icon, pos); 373 } else { 374 NamedIcon clone = new NamedIcon(icon.getImage()); 375 clone.scale(icon.getScale(), pos); 376 clone.rotate(icon.getDegrees(), pos); 377 return clone; 378 } 379 } 380 381 // overide where used - e.g. momentary 382 @Override 383 public void doMousePressed(JmriMouseEvent event) { 384 } 385 386 @Override 387 public void doMouseReleased(JmriMouseEvent event) { 388 } 389 390 @Override 391 public void doMouseClicked(JmriMouseEvent event) { 392 } 393 394 @Override 395 public void doMouseDragged(JmriMouseEvent event) { 396 } 397 398 @Override 399 public void doMouseMoved(JmriMouseEvent event) { 400 } 401 402 @Override 403 public void doMouseEntered(JmriMouseEvent event) { 404 } 405 406 @Override 407 public void doMouseExited(JmriMouseEvent event) { 408 } 409 410 @Override 411 public boolean storeItem() { 412 return true; 413 } 414 415 @Override 416 public boolean doViemMenu() { 417 return true; 418 } 419 420 /* 421 * ************** end Positionable methods ********************* 422 */ 423 /** 424 * ************************************************************* 425 */ 426 PositionablePopupUtil _popupUtil; 427 428 @Override 429 public void setPopupUtility(PositionablePopupUtil tu) { 430 _popupUtil = tu; 431 } 432 433 @Override 434 public PositionablePopupUtil getPopupUtility() { 435 return _popupUtil; 436 } 437 438 /** 439 * Update the AWT and Swing size information due to change in internal 440 * state, e.g. if one or more of the icons that might be displayed is 441 * changed 442 */ 443 @Override 444 public void updateSize() { 445 int width = maxWidth(); 446 int height = maxHeight(); 447 log.trace("updateSize() w= {}, h= {} _namedIcon= {}", width, height, _namedIcon); 448 449 setSize(width, height); 450 if (_namedIcon != null && _text) { 451 //we have a combined icon/text therefore the icon is central to the text. 452 setHorizontalTextPosition(CENTER); 453 } 454 } 455 456 @Override 457 public int maxWidth() { 458 if (_rotateText && _namedIcon != null) { 459 return _namedIcon.getIconWidth(); 460 } 461 if (_popupUtil == null) { 462 return maxWidthTrue(); 463 } 464 465 switch (_popupUtil.getOrientation()) { 466 case PositionablePopupUtil.VERTICAL_DOWN: 467 case PositionablePopupUtil.VERTICAL_UP: 468 return maxHeightTrue(); 469 default: 470 return maxWidthTrue(); 471 } 472 } 473 474 @Override 475 public int maxHeight() { 476 if (_rotateText && _namedIcon != null) { 477 return _namedIcon.getIconHeight(); 478 } 479 if (_popupUtil == null) { 480 return maxHeightTrue(); 481 } 482 switch (_popupUtil.getOrientation()) { 483 case PositionablePopupUtil.VERTICAL_DOWN: 484 case PositionablePopupUtil.VERTICAL_UP: 485 return maxWidthTrue(); 486 default: 487 return maxHeightTrue(); 488 } 489 } 490 491 public int maxWidthTrue() { 492 int result = 0; 493 if (_popupUtil != null && _popupUtil.getFixedWidth() != 0) { 494 result = _popupUtil.getFixedWidth(); 495 result += _popupUtil.getBorderSize() * 2; 496 if (result < PositionablePopupUtil.MIN_SIZE) { // don't let item disappear 497 _popupUtil.setFixedWidth(PositionablePopupUtil.MIN_SIZE); 498 result = PositionablePopupUtil.MIN_SIZE; 499 } 500 } else { 501 if (_text && getText() != null) { 502 if (getText().trim().length() == 0) { 503 // show width of 1 blank character 504 if (getFont() != null) { 505 result = getFontMetrics(getFont()).stringWidth("0"); 506 } 507 } else { 508 result = getFontMetrics(getFont()).stringWidth(getText()); 509 } 510 } 511 if (_icon && _namedIcon != null) { 512 result = Math.max(_namedIcon.getIconWidth(), result); 513 } 514 if (_text && _popupUtil != null) { 515 result += _popupUtil.getMargin() * 2; 516 result += _popupUtil.getBorderSize() * 2; 517 } 518 if (result < PositionablePopupUtil.MIN_SIZE) { // don't let item disappear 519 result = PositionablePopupUtil.MIN_SIZE; 520 } 521 } 522 if (log.isTraceEnabled()) { // avoid AWT size computation 523 log.trace("maxWidth= {} preferred width= {}", result, getPreferredSize().width); 524 } 525 return result; 526 } 527 528 public int maxHeightTrue() { 529 int result = 0; 530 if (_popupUtil != null && _popupUtil.getFixedHeight() != 0) { 531 result = _popupUtil.getFixedHeight(); 532 result += _popupUtil.getBorderSize() * 2; 533 if (result < PositionablePopupUtil.MIN_SIZE) { // don't let item disappear 534 _popupUtil.setFixedHeight(PositionablePopupUtil.MIN_SIZE); 535 } 536 } else { 537 //if(_text) { 538 if (_text && getText() != null && getFont() != null) { 539 result = getFontMetrics(getFont()).getHeight(); 540 } 541 if (_icon && _namedIcon != null) { 542 result = Math.max(_namedIcon.getIconHeight(), result); 543 } 544 if (_text && _popupUtil != null) { 545 result += _popupUtil.getMargin() * 2; 546 result += _popupUtil.getBorderSize() * 2; 547 } 548 if (result < PositionablePopupUtil.MIN_SIZE) { // don't let item disappear 549 result = PositionablePopupUtil.MIN_SIZE; 550 } 551 } 552 if (log.isTraceEnabled()) { // avoid AWT size computation 553 log.trace("maxHeight= {} preferred height= {}", result, getPreferredSize().height); 554 } 555 return result; 556 } 557 558 public boolean isBackground() { 559 return (_displayLevel == Editor.BKG); 560 } 561 562 public boolean isRotated() { 563 return _rotateText; 564 } 565 566 public void updateIcon(NamedIcon s) { 567 ThreadingUtil.runOnLayoutEventually(() -> { 568 _namedIcon = s; 569 super.setIcon(_namedIcon); 570 updateSize(); 571 repaint(); 572 }); 573 } 574 575 /* 576 * ***** Methods to add menu items to popup ******* 577 */ 578 579 /** 580 * Call to a Positionable that has unique requirements - e.g. 581 * RpsPositionIcon, SecurityElementIcon 582 */ 583 @Override 584 public boolean showPopUp(JPopupMenu popup) { 585 return false; 586 } 587 588 /** 589 * Rotate othogonally return true if popup is set 590 */ 591 @Override 592 public boolean setRotateOrthogonalMenu(JPopupMenu popup) { 593 594 if (isIcon() && (_displayLevel > Editor.BKG) && (_namedIcon != null)) { 595 popup.add(new AbstractAction(Bundle.getMessage("RotateOrthoSign", 596 (_namedIcon.getRotation() * 90))) { // Bundle property includes degree symbol 597 @Override 598 public void actionPerformed(ActionEvent e) { 599 rotateOrthogonal(); 600 } 601 }); 602 return true; 603 } 604 return false; 605 } 606 607 protected void rotateOrthogonal() { 608 _namedIcon.setRotation(_namedIcon.getRotation() + 1, this); 609 super.setIcon(_namedIcon); 610 updateSize(); 611 repaint(); 612 } 613 614 /* 615 * ********** Methods for Item Popups in Panel editor ************************ 616 */ 617 JFrame _iconEditorFrame; 618 IconAdder _iconEditor; 619 620 @Override 621 public boolean setEditIconMenu(JPopupMenu popup) { 622 if (_icon && !_text) { 623 String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("Icon")); 624 popup.add(new AbstractAction(txt) { 625 626 @Override 627 public void actionPerformed(ActionEvent e) { 628 edit(); 629 } 630 }); 631 return true; 632 } 633 return false; 634 } 635 636 /** 637 * For item popups in Panel Editor. 638 * 639 * @param pos the container 640 * @param name the name 641 * @param table true if creating a table; false otherwise 642 * @param editor the associated editor 643 */ 644 protected void makeIconEditorFrame(Container pos, String name, boolean table, IconAdder editor) { 645 if (editor != null) { 646 _iconEditor = editor; 647 } else { 648 _iconEditor = new IconAdder(name); 649 } 650 _iconEditorFrame = _editor.makeAddIconFrame(name, false, table, _iconEditor); 651 _iconEditorFrame.addWindowListener(new java.awt.event.WindowAdapter() { 652 @Override 653 public void windowClosing(java.awt.event.WindowEvent e) { 654 _iconEditorFrame.dispose(); 655 _iconEditorFrame = null; 656 } 657 }); 658 _iconEditorFrame.setLocationRelativeTo(pos); 659 _iconEditorFrame.toFront(); 660 _iconEditorFrame.setVisible(true); 661 } 662 663 protected void edit() { 664 makeIconEditorFrame(this, "Icon", false, null); 665 NamedIcon icon = new NamedIcon(_namedIcon); 666 _iconEditor.setIcon(0, "plainIcon", icon); 667 _iconEditor.makeIconPanel(false); 668 669 ActionListener addIconAction = (ActionEvent a) -> editIcon(); 670 _iconEditor.complete(addIconAction, true, false, true); 671 672 } 673 674 protected void editIcon() { 675 String url = _iconEditor.getIcon("plainIcon").getURL(); 676 _namedIcon = NamedIcon.getIconByName(url); 677 super.setIcon(_namedIcon); 678 updateSize(); 679 _iconEditorFrame.dispose(); 680 _iconEditorFrame = null; 681 _iconEditor = null; 682 invalidate(); 683 repaint(); 684 } 685 686 public jmri.jmrit.display.DisplayFrame _paletteFrame; 687 688 // ********** Methods for Item Popups in Control Panel editor ******************* 689 /** 690 * Create a palette window. 691 * 692 * @param title the name of the palette 693 * @return DisplayFrame for palette item 694 */ 695 public DisplayFrame makePaletteFrame(String title) { 696 jmri.jmrit.display.palette.ItemPalette.loadIcons(); 697 DisplayFrame frame = new DisplayFrame(title, _editor); 698 return frame; 699 } 700 701 public void initPaletteFrame(DisplayFrame paletteFrame, @Nonnull ItemPanel itemPanel) { 702 Dimension dim = itemPanel.getPreferredSize(); 703 JScrollPane sp = new JScrollPane(itemPanel); 704 dim = new Dimension(dim.width + 25, dim.height + 25); 705 sp.setPreferredSize(dim); 706 paletteFrame.add(sp); 707 paletteFrame.pack(); 708 paletteFrame.addWindowListener(new PaletteFrameCloser(itemPanel)); 709 710 jmri.InstanceManager.getDefault(jmri.util.PlaceWindow.class).nextTo(_editor, this, paletteFrame); 711 paletteFrame.setVisible(true); 712 } 713 714 static class PaletteFrameCloser extends java.awt.event.WindowAdapter { 715 ItemPanel ip; 716 PaletteFrameCloser( @Nonnull ItemPanel itemPanel) { 717 super(); 718 ip = itemPanel; 719 } 720 @Override 721 public void windowClosing(java.awt.event.WindowEvent e) { 722 ip.closeDialogs(); 723 } 724 } 725 726 public void finishItemUpdate(DisplayFrame paletteFrame, @Nonnull ItemPanel itemPanel) { 727 itemPanel.closeDialogs(); 728 paletteFrame.dispose(); 729 invalidate(); 730 } 731 732 @Override 733 public boolean setEditItemMenu(@Nonnull JPopupMenu popup) { 734 if (!_icon) { 735 return false; 736 } 737 String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("Icon")); 738 popup.add(new AbstractAction(txt) { 739 740 @Override 741 public void actionPerformed(ActionEvent e) { 742 editIconItem(); 743 } 744 }); 745 return true; 746 } 747 748 IconItemPanel _iconItemPanel; 749 750 protected void editIconItem() { 751 _paletteFrame = makePaletteFrame( 752 java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameTurnout"))); 753 _iconItemPanel = new IconItemPanel(_paletteFrame, "Icon"); // NOI18N 754 ActionListener updateAction = (ActionEvent a) -> updateIconItem(); 755 _iconItemPanel.init(updateAction); 756 _iconItemPanel.setUpdateIcon((NamedIcon)getIcon()); 757 initPaletteFrame(_paletteFrame, _iconItemPanel); 758 } 759 760 private void updateIconItem() { 761 NamedIcon icon = _iconItemPanel.getUpdateIcon(); 762 if (icon != null) { 763 String url = icon.getURL(); 764 setIcon(NamedIcon.getIconByName(url)); 765 updateSize(); 766 } 767 finishItemUpdate(_paletteFrame, _iconItemPanel); 768 } 769 770 /* Case for isIcon 771 @Override 772 public boolean setEditItemMenu(JPopupMenu popup) { 773 return setEditIconMenu(popup); 774 }*/ 775 776 public boolean setEditTextItemMenu(JPopupMenu popup) { 777 popup.add(new AbstractAction(Bundle.getMessage("SetTextSizeColor")) { 778 @Override 779 public void actionPerformed(ActionEvent e) { 780 editTextItem(); 781 } 782 }); 783 return true; 784 } 785 786 TextItemPanel _itemPanel; 787 788 protected void editTextItem() { 789 _paletteFrame = makePaletteFrame(Bundle.getMessage("SetTextSizeColor")); 790 _itemPanel = new TextItemPanel(_paletteFrame, "Text"); 791 ActionListener updateAction = (ActionEvent a) -> updateTextItem(); 792 _itemPanel.init(updateAction, this); 793 initPaletteFrame(_paletteFrame, _itemPanel); 794 } 795 796 protected void updateTextItem() { 797 PositionablePopupUtil util = _itemPanel.getPositionablePopupUtil(); 798 _itemPanel.setAttributes(this); 799 if (_editor._selectionGroup != null) { 800 _editor.setSelectionsAttributes(util, this); 801 } else { 802 _editor.setAttributes(util, this); 803 } 804 finishItemUpdate(_paletteFrame, _itemPanel); 805 } 806 807 /** 808 * Rotate degrees return true if popup is set. 809 */ 810 @Override 811 public boolean setRotateMenu(@Nonnull JPopupMenu popup) { 812 if (_displayLevel > Editor.BKG) { 813 popup.add(CoordinateEdit.getRotateEditAction(this)); 814 } 815 return false; 816 } 817 818 /** 819 * Scale percentage form display. 820 * 821 * @return true if popup is set 822 */ 823 @Override 824 public boolean setScaleMenu(@Nonnull JPopupMenu popup) { 825 if (isIcon() && _displayLevel > Editor.BKG) { 826 popup.add(CoordinateEdit.getScaleEditAction(this)); 827 return true; 828 } 829 return false; 830 } 831 832 @Override 833 public boolean setTextEditMenu(@Nonnull JPopupMenu popup) { 834 if (isText()) { 835 popup.add(CoordinateEdit.getTextEditAction(this, "EditText")); 836 return true; 837 } 838 return false; 839 } 840 841 JCheckBoxMenuItem disableItem = null; 842 843 @Override 844 public boolean setDisableControlMenu(@Nonnull JPopupMenu popup) { 845 if (_control) { 846 disableItem = new JCheckBoxMenuItem(Bundle.getMessage("Disable")); 847 disableItem.setSelected(!_controlling); 848 popup.add(disableItem); 849 disableItem.addActionListener((java.awt.event.ActionEvent e) -> setControlling(!disableItem.isSelected())); 850 return true; 851 } 852 return false; 853 } 854 855 @Override 856 public void setScale(double s) { 857 if (_namedIcon != null) { 858 _namedIcon.scale(s, this); 859 super.setIcon(_namedIcon); 860 updateSize(); 861 repaint(); 862 } 863 } 864 865 @Override 866 public double getScale() { 867 if (_namedIcon == null) { 868 return 1.0; 869 } 870 return ((NamedIcon) getIcon()).getScale(); 871 } 872 873 public void setIcon(NamedIcon icon) { 874 _namedIcon = icon; 875 super.setIcon(icon); 876 } 877 878 @Override 879 public void rotate(int deg) { 880 if (log.isDebugEnabled()) { 881 log.debug("rotate({}) with _rotateText {}, _text {}, _icon {}", deg, _rotateText, _text, _icon); 882 } 883 _degrees = deg; 884 885 if ((deg != 0) && (_popupUtil.getOrientation() != PositionablePopupUtil.HORIZONTAL)) { 886 _popupUtil.setOrientation(PositionablePopupUtil.HORIZONTAL); 887 } 888 889 if (_rotateText || deg == 0) { 890 if (deg == 0) { // restore unrotated whatever 891 _rotateText = false; 892 if (_text) { 893 if (log.isDebugEnabled()) { 894 log.debug(" super.setText(\"{}\");", _unRotatedText); 895 } 896 super.setText(_unRotatedText); 897 if (_popupUtil != null) { 898 setOpaque(_popupUtil.hasBackground()); 899 _popupUtil.setBorder(true); 900 } 901 if (_namedIcon != null) { 902 String url = _namedIcon.getURL(); 903 if (url == null) { 904 if (_text & _icon) { // create new text over icon 905 _namedIcon = makeTextOverlaidIcon(_unRotatedText, _namedIcon); 906 _namedIcon.rotate(deg, this); 907 } else if (_text) { 908 _namedIcon = null; 909 } 910 } else { 911 _namedIcon = new NamedIcon(url, url); 912 } 913 } 914 super.setIcon(_namedIcon); 915 } else { 916 if (_namedIcon != null) { 917 _namedIcon.rotate(deg, this); 918 } 919 super.setIcon(_namedIcon); 920 } 921 } else { 922 if (_text & _icon) { // update text over icon 923 _namedIcon = makeTextOverlaidIcon(_unRotatedText, _namedIcon); 924 } else if (_text) { // update text only icon image 925 _namedIcon = makeTextIcon(_unRotatedText); 926 } 927 if (_namedIcon != null) { 928 _namedIcon.rotate(deg, this); 929 super.setIcon(_namedIcon); 930 setOpaque(false); // rotations cannot be opaque 931 } 932 } 933 } else { // first time text or icon is rotated from horizontal 934 if (_text && _icon) { // text overlays icon e.g. LocoIcon 935 _namedIcon = makeTextOverlaidIcon(_unRotatedText, _namedIcon); 936 super.setText(null); 937 _rotateText = true; 938 setOpaque(false); 939 } else if (_text) { 940 _namedIcon = makeTextIcon(_unRotatedText); 941 super.setText(null); 942 _rotateText = true; 943 setOpaque(false); 944 } 945 if (_popupUtil != null) { 946 _popupUtil.setBorder(false); 947 } 948 if (_namedIcon != null) { // it is possible that the icon did not get created yet. 949 _namedIcon.rotate(deg, this); 950 super.setIcon(_namedIcon); 951 } 952 } 953 updateSize(); 954 repaint(); 955 } // rotate 956 957 /** 958 * Create an image of icon with overlaid text. 959 * 960 * @param text the text to overlay 961 * @param ic the icon containing the image 962 * @return the icon overlaying text on ic 963 */ 964 protected NamedIcon makeTextOverlaidIcon(String text, @Nonnull NamedIcon ic) { 965 String url = ic.getURL(); 966 if (url == null) { 967 return null; 968 } 969 NamedIcon icon = new NamedIcon(url, url); 970 971 int iconWidth = icon.getIconWidth(); 972 int iconHeight = icon.getIconHeight(); 973 974 int textWidth = getFontMetrics(getFont()).stringWidth(text); 975 int textHeight = getFontMetrics(getFont()).getHeight(); 976 977 int width = Math.max(textWidth, iconWidth); 978 int height = Math.max(textHeight, iconHeight); 979 980 int hOffset = Math.max((textWidth - iconWidth) / 2, 0); 981 int vOffset = Math.max((textHeight - iconHeight) / 2, 0); 982 983 if (_popupUtil != null) { 984 if (_popupUtil.getFixedWidth() != 0) { 985 switch (_popupUtil.getJustification()) { 986 case PositionablePopupUtil.LEFT: 987 hOffset = _popupUtil.getBorderSize(); 988 break; 989 case PositionablePopupUtil.RIGHT: 990 hOffset = _popupUtil.getFixedWidth() - width; 991 hOffset += _popupUtil.getBorderSize(); 992 break; 993 default: 994 hOffset = Math.max((_popupUtil.getFixedWidth() - width) / 2, 0); 995 hOffset += _popupUtil.getBorderSize(); 996 break; 997 } 998 width = _popupUtil.getFixedWidth() + 2 * _popupUtil.getBorderSize(); 999 } else { 1000 width += 2 * (_popupUtil.getMargin() + _popupUtil.getBorderSize()); 1001 hOffset += _popupUtil.getMargin() + _popupUtil.getBorderSize(); 1002 } 1003 if (_popupUtil.getFixedHeight() != 0) { 1004 vOffset = Math.max(vOffset + (_popupUtil.getFixedHeight() - height) / 2, 0); 1005 vOffset += _popupUtil.getBorderSize(); 1006 height = _popupUtil.getFixedHeight() + 2 * _popupUtil.getBorderSize(); 1007 } else { 1008 height += 2 * (_popupUtil.getMargin() + _popupUtil.getBorderSize()); 1009 vOffset += _popupUtil.getMargin() + _popupUtil.getBorderSize(); 1010 } 1011 } 1012 1013 BufferedImage bufIm = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 1014 Graphics2D g2d = bufIm.createGraphics(); 1015 g2d.setRenderingHint(RenderingHints.KEY_RENDERING, 1016 RenderingHints.VALUE_RENDER_QUALITY); 1017 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 1018 RenderingHints.VALUE_ANTIALIAS_ON); 1019 g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, 1020 RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); 1021// g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, // Turned off due to poor performance, see Issue #3850 and PR #3855 for background 1022// RenderingHints.VALUE_INTERPOLATION_BICUBIC); 1023 1024 if (_popupUtil != null) { 1025 if (_popupUtil.hasBackground()) { 1026 g2d.setColor(_popupUtil.getBackground()); 1027 g2d.fillRect(0, 0, width, height); 1028 } 1029 if (_popupUtil.getBorderSize() != 0) { 1030 g2d.setColor(_popupUtil.getBorderColor()); 1031 g2d.setStroke(new java.awt.BasicStroke(2 * _popupUtil.getBorderSize())); 1032 g2d.drawRect(0, 0, width, height); 1033 } 1034 } 1035 1036 g2d.drawImage(icon.getImage(), AffineTransform.getTranslateInstance(hOffset, vOffset + 1), this); 1037 1038 icon = new NamedIcon(bufIm); 1039 g2d.dispose(); 1040 icon.setURL(url); 1041 return icon; 1042 } 1043 1044 /** 1045 * Create a text image whose bit map can be rotated. 1046 */ 1047 private NamedIcon makeTextIcon(String text) { 1048 if (text == null || text.equals("")) { 1049 text = " "; 1050 } 1051 int width = getFontMetrics(getFont()).stringWidth(text); 1052 int height = getFontMetrics(getFont()).getHeight(); 1053 // int hOffset = 0; // variable has no effect, see Issue #5662 1054 // int vOffset = getFontMetrics(getFont()).getAscent(); 1055 if (_popupUtil != null) { 1056 if (_popupUtil.getFixedWidth() != 0) { 1057 switch (_popupUtil.getJustification()) { 1058 case PositionablePopupUtil.LEFT: 1059 // hOffset = _popupUtil.getBorderSize(); // variable has no effect, see Issue #5662 1060 break; 1061 case PositionablePopupUtil.RIGHT: 1062 // hOffset = _popupUtil.getFixedWidth() - width; // variable has no effect, see Issue #5662 1063 // hOffset += _popupUtil.getBorderSize(); // variable has no effect, see Issue #5662 1064 break; 1065 default: 1066 // hOffset = Math.max((_popupUtil.getFixedWidth() - width) / 2, 0); // variable has no effect, see Issue #5662 1067 // hOffset += _popupUtil.getBorderSize(); // variable has no effect, see Issue #5662 1068 break; 1069 } 1070 width = _popupUtil.getFixedWidth() + 2 * _popupUtil.getBorderSize(); 1071 } else { 1072 width += 2 * (_popupUtil.getMargin() + _popupUtil.getBorderSize()); 1073 // hOffset += _popupUtil.getMargin() + _popupUtil.getBorderSize(); // variable has no effect, see Issue #5662 1074 } 1075 if (_popupUtil.getFixedHeight() != 0) { 1076 // vOffset = Math.max(vOffset + (_popupUtil.getFixedHeight() - height) / 2, 0); 1077 // vOffset += _popupUtil.getBorderSize(); 1078 height = _popupUtil.getFixedHeight() + 2 * _popupUtil.getBorderSize(); 1079 } else { 1080 height += 2 * (_popupUtil.getMargin() + _popupUtil.getBorderSize()); 1081 // vOffset += _popupUtil.getMargin() + _popupUtil.getBorderSize(); 1082 } 1083 } 1084 1085 BufferedImage bufIm = new BufferedImage(width + 2, height + 2, BufferedImage.TYPE_INT_ARGB); 1086 Graphics2D g2d = bufIm.createGraphics(); 1087 1088 g2d.setBackground(new Color(0, 0, 0, 0)); 1089 g2d.clearRect(0, 0, bufIm.getWidth(), bufIm.getHeight()); 1090 1091 g2d.setFont(getFont()); 1092 g2d.setRenderingHint(RenderingHints.KEY_RENDERING, 1093 RenderingHints.VALUE_RENDER_QUALITY); 1094 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 1095 RenderingHints.VALUE_ANTIALIAS_ON); 1096 g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, 1097 RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); 1098// g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, // Turned off due to poor performance, see Issue #3850 and PR #3855 for background 1099// RenderingHints.VALUE_INTERPOLATION_BICUBIC); 1100 1101 if (_popupUtil != null) { 1102 if (_popupUtil.hasBackground()) { 1103 g2d.setColor(_popupUtil.getBackground()); 1104 g2d.fillRect(0, 0, width, height); 1105 } 1106 if (_popupUtil.getBorderSize() != 0) { 1107 g2d.setColor(_popupUtil.getBorderColor()); 1108 g2d.setStroke(new java.awt.BasicStroke(2 * _popupUtil.getBorderSize())); 1109 g2d.drawRect(0, 0, width, height); 1110 } 1111 } 1112 1113 NamedIcon icon = new NamedIcon(bufIm); 1114 g2d.dispose(); 1115 return icon; 1116 } 1117 1118 public void setDegrees(int deg) { 1119 _degrees = deg; 1120 } 1121 1122 @Override 1123 public int getDegrees() { 1124 return _degrees; 1125 } 1126 1127 /** 1128 * Clean up when this object is no longer needed. Should not be called while 1129 * the object is still displayed; see remove() 1130 */ 1131 public void dispose() { 1132 } 1133 1134 /** 1135 * Removes this object from display and persistance 1136 */ 1137 @Override 1138 public void remove() { 1139 // If this Positionable has an Inline LogixNG, that LogixNG might be in use. 1140 LogixNG logixNG = getLogixNG(); 1141 if (logixNG != null) { 1142 DeleteBean<LogixNG> deleteBean = new DeleteBean<>( 1143 InstanceManager.getDefault(LogixNG_Manager.class)); 1144 1145 boolean hasChildren = logixNG.getNumConditionalNGs() > 0; 1146 1147 deleteBean.delete(logixNG, hasChildren, (t)->{deleteLogixNG(t);}, 1148 (t,list)->{logixNG.getListenerRefsIncludingChildren(list);}, 1149 jmri.jmrit.logixng.LogixNG_UserPreferences.class.getName()); 1150 } else { 1151 doRemove(); 1152 } 1153 } 1154 1155 private void deleteLogixNG(LogixNG logixNG) { 1156 logixNG.setEnabled(false); 1157 try { 1158 InstanceManager.getDefault(LogixNG_Manager.class).deleteBean(logixNG, "DoDelete"); 1159 setLogixNG(null); 1160 doRemove(); 1161 } catch (PropertyVetoException e) { 1162 //At this stage the DoDelete shouldn't fail, as we have already done a can delete, which would trigger a veto 1163 log.error("{} : Could not Delete.", e.getMessage()); 1164 } 1165 } 1166 1167 private void doRemove() { 1168 if (_editor.removeFromContents(this)) { 1169 // Modified to support conditional delete for NX sensors 1170 // remove from persistance by flagging inactive 1171 active = false; 1172 dispose(); 1173 } 1174 } 1175 1176 boolean active = true; 1177 1178 /** 1179 * Check if the component is still displayed, and should be stored. 1180 * 1181 * @return true if active; false otherwise 1182 */ 1183 public boolean isActive() { 1184 return active; 1185 } 1186 1187 protected void setSuperText(String text) { 1188 _unRotatedText = text; 1189 super.setText(text); 1190 } 1191 1192 @Override 1193 public void setText(String text) { 1194 if (this instanceof BlockContentsIcon || this instanceof MemoryIcon || this instanceof GlobalVariableIcon) { 1195 if (_editor != null && !_editor.isEditable()) { 1196 if (isEmptyHidden()) { 1197 log.debug("label setText: {} :: {}", text, getNameString()); 1198 if (text == null || text.isEmpty()) { 1199 setVisible(false); 1200 } else { 1201 setVisible(true); 1202 } 1203 } 1204 } 1205 } 1206 1207 _unRotatedText = text; 1208 _text = (text != null && text.length() > 0); // when "" is entered for text, and a font has been specified, the descender distance moves the position 1209 if (/*_rotateText &&*/!isIcon() && (_namedIcon != null || _degrees != 0)) { 1210 log.debug("setText calls rotate({})", _degrees); 1211 rotate(_degrees); //this will change text label as a icon with a new _namedIcon. 1212 } else { 1213 log.debug("setText calls super.setText()"); 1214 super.setText(text); 1215 } 1216 } 1217 1218 private boolean needsRotate; 1219 1220 @Override 1221 public Dimension getSize() { 1222 if (!needsRotate) { 1223 return super.getSize(); 1224 } 1225 1226 Dimension size = super.getSize(); 1227 if (_popupUtil == null) { 1228 return super.getSize(); 1229 } 1230 switch (_popupUtil.getOrientation()) { 1231 case PositionablePopupUtil.VERTICAL_DOWN: 1232 case PositionablePopupUtil.VERTICAL_UP: 1233 if (_degrees != 0) { 1234 rotate(0); 1235 } 1236 return new Dimension(size.height, size.width); // flip dimension 1237 default: 1238 return super.getSize(); 1239 } 1240 } 1241 1242 @Override 1243 public int getHeight() { 1244 return getSize().height; 1245 } 1246 1247 @Override 1248 public int getWidth() { 1249 return getSize().width; 1250 } 1251 1252 @Override 1253 protected void paintComponent(Graphics g) { 1254 if (_popupUtil == null) { 1255 super.paintComponent(g); 1256 } else { 1257 Graphics2D g2d = (Graphics2D) g.create(); 1258 1259 // set antialiasing hint for macOS and Windows 1260 // note: antialiasing has performance problems on some variants of Linux (Raspberry pi) 1261 if (SystemType.isMacOSX() || SystemType.isWindows()) { 1262 g2d.setRenderingHint(RenderingHints.KEY_RENDERING, 1263 RenderingHints.VALUE_RENDER_QUALITY); 1264 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 1265 RenderingHints.VALUE_ANTIALIAS_ON); 1266 g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, 1267 RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); 1268// g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, // Turned off due to poor performance, see Issue #3850 and PR #3855 for background 1269// RenderingHints.VALUE_INTERPOLATION_BICUBIC); 1270 } 1271 1272 switch (_popupUtil.getOrientation()) { 1273 case PositionablePopupUtil.VERTICAL_UP: 1274 g2d.translate(0, getSize().getHeight()); 1275 g2d.transform(AffineTransform.getQuadrantRotateInstance(-1)); 1276 break; 1277 case PositionablePopupUtil.VERTICAL_DOWN: 1278 g2d.transform(AffineTransform.getQuadrantRotateInstance(1)); 1279 g2d.translate(0, -getSize().getWidth()); 1280 break; 1281 case 0: 1282 // routine value (not initialized) for no change 1283 break; 1284 default: 1285 // unexpected orientation value 1286 jmri.util.LoggingUtil.warnOnce(log, "Unexpected orientation = {}", _popupUtil.getOrientation()); 1287 break; 1288 } 1289 1290 needsRotate = true; 1291 super.paintComponent(g2d); 1292 needsRotate = false; 1293 1294 if (_popupUtil.getOrientation() == PositionablePopupUtil.HORIZONTAL) { 1295 if ((_unRotatedText != null) && (_degrees != 0)) { 1296 double angleRAD = Math.toRadians(_degrees); 1297 1298 int iconWidth = getWidth(); 1299 int iconHeight = getHeight(); 1300 1301 int textWidth = getFontMetrics(getFont()).stringWidth(_unRotatedText); 1302 int textHeight = getFontMetrics(getFont()).getHeight(); 1303 1304 Point2D textSizeRotated = MathUtil.rotateRAD(textWidth, textHeight, angleRAD); 1305 int textWidthRotated = (int) textSizeRotated.getX(); 1306 int textHeightRotated = (int) textSizeRotated.getY(); 1307 1308 int width = Math.max(textWidthRotated, iconWidth); 1309 int height = Math.max(textHeightRotated, iconHeight); 1310 1311 int iconOffsetX = width / 2; 1312 int iconOffsetY = height / 2; 1313 1314 g2d.transform(AffineTransform.getRotateInstance(angleRAD, iconOffsetX, iconOffsetY)); 1315 1316 int hOffset = iconOffsetX - (textWidth / 2); 1317 //int vOffset = iconOffsetY + ((textHeight - getFontMetrics(getFont()).getAscent()) / 2); 1318 int vOffset = iconOffsetY + (textHeight / 4); // why 4? Don't know, it just looks better 1319 1320 g2d.setFont(getFont()); 1321 g2d.setColor(getForeground()); 1322 g2d.drawString(_unRotatedText, hOffset, vOffset); 1323 } 1324 } 1325 } 1326 } // paintComponent 1327 1328 /** 1329 * Provide a generic method to return the bean associated with the 1330 * Positionable. 1331 */ 1332 @Override 1333 public jmri.NamedBean getNamedBean() { 1334 return null; 1335 } 1336 1337 /** {@inheritDoc} */ 1338 @Override 1339 public LogixNG getLogixNG() { 1340 return _logixNG; 1341 } 1342 1343 /** {@inheritDoc} */ 1344 @Override 1345 public void setLogixNG(LogixNG logixNG) { 1346 this._logixNG = logixNG; 1347 } 1348 1349 /** {@inheritDoc} */ 1350 @Override 1351 public void setLogixNG_SystemName(String systemName) { 1352 this._logixNG_SystemName = systemName; 1353 } 1354 1355 /** {@inheritDoc} */ 1356 @Override 1357 public void setupLogixNG() { 1358 _logixNG = InstanceManager.getDefault(LogixNG_Manager.class) 1359 .getBySystemName(_logixNG_SystemName); 1360 if (_logixNG == null) { 1361 throw new RuntimeException(String.format( 1362 "LogixNG %s is not found for positional %s in panel %s", 1363 _logixNG_SystemName, getNameString(), getEditor().getName())); 1364 } 1365 _logixNG.setInlineLogixNG(this); 1366 } 1367 1368 private final static Logger log = LoggerFactory.getLogger(PositionableLabel.class); 1369 1370}