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}