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