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