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