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