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