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