001package jmri.jmrit.display.configurexml;
002
003import jmri.util.gui.GuiLafPreferencesManager;
004
005import java.awt.Color;
006import java.awt.Font;
007
008import jmri.InstanceManager;
009import jmri.configurexml.AbstractXmlAdapter;
010import jmri.configurexml.JmriConfigureXmlException;
011import jmri.jmrit.catalog.NamedIcon;
012import jmri.jmrit.display.Editor;
013import jmri.jmrit.display.Positionable;
014import jmri.jmrit.display.PositionableLabel;
015import jmri.jmrit.display.PositionablePopupUtil;
016import jmri.jmrit.display.ToolTip;
017import jmri.jmrit.logixng.LogixNG_Manager;
018
019import org.jdom2.Attribute;
020import org.jdom2.DataConversionException;
021import org.jdom2.Element;
022import org.slf4j.Logger;
023import org.slf4j.LoggerFactory;
024
025/**
026 * Handle configuration for display.PositionableLabel objects
027 *
028 * @author Bob Jacobsen Copyright: Copyright (c) 2002
029 */
030public class PositionableLabelXml extends AbstractXmlAdapter {
031
032    public PositionableLabelXml() {
033    }
034
035    /**
036     * Default implementation for storing the contents of a PositionableLabel
037     *
038     * @param o Object to store, of type PositionableLabel
039     * @return Element containing the complete info
040     */
041    @Override
042    public Element store(Object o) {
043        PositionableLabel p = (PositionableLabel) o;
044
045        if (!p.isActive()) {
046            return null;  // if flagged as inactive, don't store
047        }
048        Element element = new Element("positionablelabel");
049        storeCommonAttributes(p, element);
050
051        if (p.isText()) {
052            if (p.getUnRotatedText() != null) {
053                element.setAttribute("text", p.getUnRotatedText());
054            }
055            storeTextInfo(p, element);
056        }
057
058        if (p.isIcon() && p.getIcon() != null) {
059            element.setAttribute("icon", "yes");
060            element.addContent(storeIcon("icon", (NamedIcon) p.getIcon()));
061        }
062
063        storeLogixNG_Data(p, element);
064
065        element.setAttribute("class", "jmri.jmrit.display.configurexml.PositionableLabelXml");
066        return element;
067    }
068
069    /**
070     * Store the text formatting information.
071     * <p>
072     * This is always stored, even if the icon isn't in text mode, because some
073     * uses (subclasses) of PositionableLabel flip back and forth between icon
074     * and text, and want to remember their formatting.
075     *
076     * @param p       the icon to store
077     * @param element the XML representation of the icon
078     */
079    protected void storeTextInfo(Positionable p, Element element) {
080        //if (p.getText()!=null) element.setAttribute("text", p.getText());
081        PositionablePopupUtil util = p.getPopupUtility();
082
083        GuiLafPreferencesManager manager = InstanceManager.getDefault(GuiLafPreferencesManager.class);
084        String defaultFontName = manager.getDefaultFont().getFontName();
085
086        String fontName = util.getFont().getFontName();
087        if (!fontName.equals(defaultFontName)) {
088            element.setAttribute("fontFamily", "" + util.getFont().getFamily());
089            element.setAttribute("fontname", "" + fontName);
090        }
091
092        element.setAttribute("size", "" + util.getFontSize());
093        element.setAttribute("style", "" + util.getFontStyle());
094
095        // always write the foreground (text) color
096        element.setAttribute("red", "" + util.getForeground().getRed());
097        element.setAttribute("green", "" + util.getForeground().getGreen());
098        element.setAttribute("blue", "" + util.getForeground().getBlue());
099
100        element.setAttribute("hasBackground", util.hasBackground() ? "yes" : "no");
101        if (util.hasBackground()) {
102            element.setAttribute("redBack", "" + util.getBackground().getRed());
103            element.setAttribute("greenBack", "" + util.getBackground().getGreen());
104            element.setAttribute("blueBack", "" + util.getBackground().getBlue());
105        }
106
107        if (util.getMargin() != 0) {
108            element.setAttribute("margin", "" + util.getMargin());
109        }
110        if (util.getBorderSize() != 0) {
111            element.setAttribute("borderSize", "" + util.getBorderSize());
112            element.setAttribute("redBorder", "" + util.getBorderColor().getRed());
113            element.setAttribute("greenBorder", "" + util.getBorderColor().getGreen());
114            element.setAttribute("blueBorder", "" + util.getBorderColor().getBlue());
115        }
116        if (util.getFixedWidth() != 0) {
117            element.setAttribute("fixedWidth", "" + util.getFixedWidth());
118        }
119        if (util.getFixedHeight() != 0) {
120            element.setAttribute("fixedHeight", "" + util.getFixedHeight());
121        }
122
123        String just;
124        switch (util.getJustification()) {
125            case 0x02:
126                just = "right";
127                break;
128            case 0x04:
129                just = "centre";
130                break;
131            default:
132                just = "left";
133                break;
134        }
135        element.setAttribute("justification", just);
136
137        if (util.getOrientation() != PositionablePopupUtil.HORIZONTAL) {
138            String ori;
139            switch (util.getOrientation()) {
140                case PositionablePopupUtil.VERTICAL_DOWN:
141                    ori = "vertical_down";
142                    break;
143                case PositionablePopupUtil.VERTICAL_UP:
144                    ori = "vertical_up";
145                    break;
146                default:
147                    ori = "horizontal";
148                    break;
149            }
150            element.setAttribute("orientation", ori);
151        }
152        //return element;
153    }
154
155    /**
156     * Default implementation for storing the common contents of an Icon
157     *
158     * @param p       the icon to store
159     * @param element the XML representation of the icon
160     */
161    public void storeCommonAttributes(Positionable p, Element element) {
162
163        if (p.getId() != null) element.setAttribute("id", p.getId());
164
165        var classes = p.getClasses();
166        if (!classes.isEmpty()) {
167            StringBuilder classNames = new StringBuilder();
168            for (String className : classes) {
169                if (className.contains(",")) {
170                    throw new UnsupportedOperationException("Comma is not allowed in class names");
171                }
172                if (classNames.length() > 0) classNames.append(",");
173                classNames.append(className);
174            }
175            element.setAttribute("classes", classNames.toString());
176        }
177
178        element.setAttribute("x", "" + p.getX());
179        element.setAttribute("y", "" + p.getY());
180        element.setAttribute("level", String.valueOf(p.getDisplayLevel()));
181        element.setAttribute("forcecontroloff", !p.isControlling() ? "true" : "false");
182        element.setAttribute("hidden", p.isHidden() ? "yes" : "no");
183        if (p.isEmptyHidden()) {
184            element.setAttribute("emptyHidden", "yes");
185        }
186        element.setAttribute("positionable", p.isPositionable() ? "true" : "false");
187        element.setAttribute("showtooltip", p.showToolTip() ? "true" : "false");
188        element.setAttribute("editable", p.isEditable() ? "true" : "false");
189        ToolTip tip = p.getToolTip();
190        if (tip != null) {
191            if (tip.getPrependToolTipWithDisplayName()) {
192                element.addContent(
193                        new Element("tooltip_prependWithDisplayName")
194                                .addContent("yes"));
195            }
196            String txt = tip.getText();
197            if (txt != null) {
198                Element elem = new Element("tooltip").addContent(txt); // was written as "toolTip" 3.5.1 and before
199                element.addContent(elem);
200            }
201        }
202        if (p.getDegrees() != 0) {
203            element.setAttribute("degrees", "" + p.getDegrees());
204        }
205    }
206
207    public Element storeIcon(String elemName, NamedIcon icon) {
208        if (icon == null) {
209            return null;
210        }
211        Element element = new Element(elemName);
212        element.setAttribute("url", icon.getURL());
213        element.setAttribute("degrees", String.valueOf(icon.getDegrees()));
214        element.setAttribute("scale", String.valueOf(icon.getScale()));
215
216        // the "rotate" attribute was deprecated in 2.9.4, replaced by the "rotation" element
217        element.addContent(new Element("rotation").addContent(String.valueOf(icon.getRotation())));
218
219        return element;
220    }
221
222    public void storeLogixNG_Data(Positionable p, Element element) {
223        if (p.getLogixNG() == null) return;
224
225        // Don't save LogixNG data if we don't have any ConditionalNGs
226        if (p.getLogixNG().getNumConditionalNGs() == 0) return;
227        Element logixNG_Element = new Element("LogixNG");
228        logixNG_Element.addContent(new Element("InlineLogixNG_SystemName").addContent(p.getLogixNG().getSystemName()));
229        element.addContent(logixNG_Element);
230    }
231
232    @Override
233    public boolean load(Element shared, Element perNode) {
234        log.error("Invalid method called");
235        return false;
236    }
237
238    /**
239     * Create a PositionableLabel, then add to a target JLayeredPane
240     *
241     * @param element Top level Element to unpack.
242     * @param o       Editor as an Object
243     * @throws JmriConfigureXmlException when a error prevents creating the objects as as
244     *                   required by the input XML
245     */
246    @Override
247    public void load(Element element, Object o) throws JmriConfigureXmlException {
248        // create the objects
249        PositionableLabel l = null;
250
251        // get object class and determine editor being used
252        Editor editor = (Editor) o;
253        if (element.getAttribute("icon") != null) {
254            NamedIcon icon;
255            String name = element.getAttribute("icon").getValue();
256//            if (log.isDebugEnabled()) log.debug("icon attribute= "+name);
257            if (name.equals("yes")) {
258                icon = getNamedIcon("icon", element, "PositionableLabel ", editor);
259            } else {
260                icon = NamedIcon.getIconByName(name);
261                if (icon == null) {
262                    icon = editor.loadFailed("PositionableLabel", name);
263                    if (icon == null) {
264                        log.info("PositionableLabel icon removed for url= {}", name);
265                        return;
266                    }
267                }
268            }
269            // abort if name != yes and have null icon
270            if (icon == null && !name.equals("yes")) {
271                log.info("PositionableLabel icon removed for url= {}", name);
272                return;
273            }
274            l = new PositionableLabel(icon, editor);
275            try {
276                Attribute a = element.getAttribute("rotate");
277                if (a != null && icon != null) {
278                    int rotation = element.getAttribute("rotate").getIntValue();
279                    icon.setRotation(rotation, l);
280                }
281            } catch (org.jdom2.DataConversionException e) {
282            }
283
284            if (name.equals("yes")) {
285                NamedIcon nIcon = loadIcon(l, "icon", element, "PositionableLabel ", editor);
286                if (nIcon != null) {
287                    l.updateIcon(nIcon);
288                } else {
289                    log.info("PositionableLabel icon removed for url= {}", name);
290                    return;
291                }
292            } else {
293                l.updateIcon(icon);
294            }
295        }
296
297        if (element.getAttribute("text") != null) {
298            if (l == null) {
299                l = new PositionableLabel(element.getAttribute("text").getValue(), editor);
300            }
301            loadTextInfo(l, element);
302
303        } else if (l == null) {
304            log.error("PositionableLabel is null!");
305            if (log.isDebugEnabled()) {
306                java.util.List<Attribute> attrs = element.getAttributes();
307                log.debug("\tElement Has {} Attributes:", attrs.size());
308                for (Attribute a : attrs) {
309                    log.debug("  attribute:  {} = {}", a.getName(), a.getValue());
310                }
311                java.util.List<Element> kids = element.getChildren();
312                log.debug("\tElementHas {} children:", kids.size());
313                for (Element e : kids) {
314                    log.debug("  child:  {} = \"{}\"", e.getName(), e.getValue());
315                }
316            }
317            editor.loadFailed();
318            return;
319        }
320        try {
321            editor.putItem(l);
322        } catch (Positionable.DuplicateIdException e) {
323            // This should never happen
324            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
325        }
326
327        loadLogixNG_Data(l, element);
328
329        // load individual item's option settings after editor has set its global settings
330        loadCommonAttributes(l, Editor.LABELS, element);
331    }
332
333    protected void loadTextInfo(Positionable l, Element element) {
334        if (log.isDebugEnabled()) {
335            log.debug("loadTextInfo");
336        }
337        jmri.jmrit.display.PositionablePopupUtil util = l.getPopupUtility();
338        if (util == null) {
339            log.warn("PositionablePopupUtil is null! {}", element);
340            return;
341        }
342
343        Attribute a = element.getAttribute("size");
344        try {
345            if (a != null) {
346                util.setFontSize(a.getFloatValue());
347            }
348        } catch (DataConversionException ex) {
349            log.warn("invalid size attribute value");
350        }
351
352        a = element.getAttribute("style");
353        try {
354            if (a != null) {
355                int style = a.getIntValue();
356                int drop = 0;
357                switch (style) {
358                    case 0:  //0 Normal
359                    case 2:  // italic
360                        drop = 1;
361                        break;
362                    default:
363                        break;
364                }
365                util.setFontStyle(style, drop);
366            }
367        } catch (DataConversionException ex) {
368            log.warn("invalid style attribute value");
369        }
370
371        a = element.getAttribute("fontname");
372        try {
373            if (a != null) {
374                util.setFont(new Font(a.getValue(), util.getFontStyle(), util.getFontSize()));
375                // Reset util to the new instance
376                // The setFont process clones the current util instance but the rest of loadTextInfo used the orignal instance.
377                util = l.getPopupUtility();
378            }
379        } catch (NullPointerException e) {  // considered normal if the attributes are not present
380        }
381
382        // set color if needed
383        try {
384            int red = element.getAttribute("red").getIntValue();
385            int blue = element.getAttribute("blue").getIntValue();
386            int green = element.getAttribute("green").getIntValue();
387            util.setForeground(new Color(red, green, blue));
388        } catch (org.jdom2.DataConversionException e) {
389            log.warn("Could not parse color attributes!");
390        } catch (NullPointerException e) {  // considered normal if the attributes are not present
391        }
392
393        a = element.getAttribute("hasBackground");
394        if (a != null) {
395            util.setHasBackground("yes".equals(a.getValue()));
396        } else {
397            util.setHasBackground(true);
398        }
399        if (util.hasBackground()) {
400            try {
401                int red = element.getAttribute("redBack").getIntValue();
402                int blue = element.getAttribute("blueBack").getIntValue();
403                int green = element.getAttribute("greenBack").getIntValue();
404                util.setBackgroundColor(new Color(red, green, blue));
405            } catch (org.jdom2.DataConversionException e) {
406                log.warn("Could not parse background color attributes!");
407            } catch (NullPointerException e) {
408                util.setHasBackground(false);// if the attributes are not listed, we consider the background as clear.
409            }
410        }
411
412        int fixedWidth = 0;
413        int fixedHeight = 0;
414        try {
415            fixedHeight = element.getAttribute("fixedHeight").getIntValue();
416        } catch (org.jdom2.DataConversionException e) {
417            log.warn("Could not parse fixed Height attributes!");
418        } catch (NullPointerException e) {  // considered normal if the attributes are not present
419        }
420
421        try {
422            fixedWidth = element.getAttribute("fixedWidth").getIntValue();
423        } catch (org.jdom2.DataConversionException e) {
424            log.warn("Could not parse fixed Width attribute!");
425        } catch (NullPointerException e) {  // considered normal if the attributes are not present
426        }
427        if (!(fixedWidth == 0 && fixedHeight == 0)) {
428            util.setFixedSize(fixedWidth, fixedHeight);
429        }
430        if ((util.getFixedWidth() == 0) || (util.getFixedHeight() == 0)) {
431            try {
432                util.setMargin(element.getAttribute("margin").getIntValue());
433            } catch (org.jdom2.DataConversionException e) {
434                log.warn("Could not parse margin attribute!");
435            } catch (NullPointerException e) {  // considered normal if the attributes are not present
436            }
437        }
438        try {
439            util.setBorderSize(element.getAttribute("borderSize").getIntValue());
440            int red = element.getAttribute("redBorder").getIntValue();
441            int blue = element.getAttribute("blueBorder").getIntValue();
442            int green = element.getAttribute("greenBorder").getIntValue();
443            util.setBorderColor(new Color(red, green, blue));
444        } catch (org.jdom2.DataConversionException e) {
445            log.warn("Could not parse border attributes!");
446        } catch (NullPointerException e) {  // considered normal if the attribute not present
447        }
448
449        a = element.getAttribute("justification");
450        if (a != null) {
451            util.setJustification(a.getValue());
452        } else {
453            util.setJustification("left");
454        }
455        a = element.getAttribute("orientation");
456        if (a != null) {
457            util.setOrientation(a.getValue());
458        } else {
459            util.setOrientation("horizontal");
460        }
461
462        int deg = 0;
463        try {
464            a = element.getAttribute("degrees");
465            if (a != null) {
466                deg = a.getIntValue();
467                l.rotate(deg);
468            }
469        } catch (DataConversionException ex) {
470            log.warn("invalid 'degrees' value (non integer)");
471        }
472        if (deg == 0 && util.hasBackground()) {
473            l.setOpaque(true);
474        }
475    }
476
477    public void loadCommonAttributes(Positionable l, int defaultLevel, Element element)
478            throws JmriConfigureXmlException {
479
480        if (element.getAttribute("id") != null) {
481            try {
482                l.setId(element.getAttribute("id").getValue());
483            } catch (Positionable.DuplicateIdException e) {
484                throw new JmriConfigureXmlException("Positionable id is not unique", e);
485            }
486        }
487
488        if (element.getAttribute("classes") != null) {
489            String classes = element.getAttribute("classes").getValue();
490            for (String className : classes.split(",")) {
491                if (!className.isBlank()) {
492                    l.addClass(className);
493                }
494            }
495        }
496
497        try {
498            l.setControlling(!element.getAttribute("forcecontroloff").getBooleanValue());
499        } catch (DataConversionException e1) {
500            log.warn("unable to convert positionable label forcecontroloff attribute");
501        } catch (Exception e) {
502        }
503
504        // find coordinates
505        int x = 0;
506        int y = 0;
507        try {
508            x = element.getAttribute("x").getIntValue();
509            y = element.getAttribute("y").getIntValue();
510        } catch (org.jdom2.DataConversionException e) {
511            log.error("failed to convert positional attribute");
512        }
513        l.setLocation(x, y);
514
515        // find display level
516        int level = defaultLevel;
517        try {
518            level = element.getAttribute("level").getIntValue();
519        } catch (org.jdom2.DataConversionException e) {
520            log.warn("Could not parse level attribute!");
521        } catch (NullPointerException e) {
522            // considered normal if the attribute not present
523        }
524        l.setDisplayLevel(level);
525
526        try {
527            boolean value = element.getAttribute("hidden").getBooleanValue();
528            l.setHidden(value);
529            l.setVisible(!value);
530        } catch (DataConversionException e) {
531            log.warn("unable to convert positionable label hidden attribute");
532        } catch (NullPointerException e) {
533            // considered normal if the attribute not present
534        }
535
536        try {
537            boolean value = element.getAttribute("emptyHidden").getBooleanValue();
538            l.setEmptyHidden(value);
539        } catch (DataConversionException e) {
540            log.warn("unable to convert positionable label emptyHidden attribute");
541        } catch (NullPointerException e) {
542            // considered normal if the attribute not present
543        }
544
545        try {
546            l.setPositionable(element.getAttribute("positionable").getBooleanValue());
547        } catch (DataConversionException e) {
548            log.warn("unable to convert positionable label positionable attribute");
549        } catch (NullPointerException e) {
550            // considered normal if the attribute not present
551        }
552        try {
553            l.setShowToolTip(element.getAttribute("showtooltip").getBooleanValue());
554        } catch (DataConversionException e) {
555            log.warn("unable to convert positionable label showtooltip attribute");
556        } catch (NullPointerException e) {
557            // considered normal if the attribute not present
558        }
559        try {
560            l.setEditable(element.getAttribute("editable").getBooleanValue());
561        } catch (DataConversionException e) {
562            log.warn("unable to convert positionable label editable attribute");
563        } catch (NullPointerException e) {
564            // considered normal if the attribute not present
565        }
566
567        Attribute a = element.getAttribute("degrees");
568        if (a != null && l instanceof PositionableLabel) {
569            try {
570                int deg = a.getIntValue();
571                ((PositionableLabel) l).setDegrees(deg);
572            } catch (org.jdom2.DataConversionException dce) {
573            }
574        }
575
576        Element elem = element.getChild("tooltip_prependWithDisplayName");
577        if (elem != null) {
578            ToolTip tip = l.getToolTip();
579            if (tip != null) {
580                tip.setPrependToolTipWithDisplayName("yes".equals(elem.getText()));
581            }
582        }
583
584        elem = element.getChild("tooltip");
585        if (elem == null) {
586            elem = element.getChild("toolTip"); // pre JMRI 3.5.2
587        }
588        if (elem != null) {
589            ToolTip tip = l.getToolTip();
590            if (tip != null) {
591                tip.setText(elem.getText());
592            }
593        }
594    }
595
596    public NamedIcon loadIcon(PositionableLabel l, String attrName, Element element,
597            String name, Editor ed) {
598        NamedIcon icon = getNamedIcon(attrName, element, name, ed);
599        if (icon != null) {
600            try {
601                int deg = 0;
602                double scale = 1.0;
603                Element elem = element.getChild(attrName);
604                if (elem != null) {
605                    Attribute a = elem.getAttribute("degrees");
606                    if (a != null) {
607                        deg = a.getIntValue();
608                    }
609                    a = elem.getAttribute("scale");
610                    if (a != null) {
611                        scale = elem.getAttribute("scale").getDoubleValue();
612                    }
613                    icon.setLoad(deg, scale, l);
614                    if (deg == 0) {
615                        // "rotate" attribute is JMRI 2.9.3 and before
616                        a = elem.getAttribute("rotate");
617                        if (a != null) {
618                            int rotation = a.getIntValue();
619                            // 2.9.3 and before, only unscaled icons rotate
620                            if (scale == 1.0) {
621                                icon.setRotation(rotation, l);
622                            }
623                        }
624                        // "rotation" element is JMRI 2.9.4 and after
625                        Element e = elem.getChild("rotation");
626                        if (e != null) {
627                            // ver 2.9.4 allows orthogonal rotations of scaled icons
628                            int rotation = Integer.parseInt(e.getText());
629                            icon.setRotation(rotation, l);
630                        }
631                    }
632                }
633            } catch (org.jdom2.DataConversionException dce) {
634            }
635        }
636        return icon;
637    }
638
639    protected NamedIcon getNamedIcon(String childName, Element element,
640            String name, Editor ed) {
641        NamedIcon icon = null;
642        Element elem = element.getChild(childName);
643        if (elem != null) {
644            String iconName = elem.getAttribute("url").getValue();
645            icon = NamedIcon.getIconByName(iconName);
646            if (icon == null) {
647                icon = ed.loadFailed(name, iconName);
648                if (icon == null) {
649                    log.info("{} removed for url= {}", name, iconName);
650                }
651            }
652        } else {
653            log.debug("getNamedIcon: child element \"{}\" not found in element {}", childName, element.getName());
654        }
655        return icon;
656    }
657
658    public void loadLogixNG_Data(Positionable p, Element element) {
659        Element logixNG_Element = element.getChild("LogixNG");
660        if (logixNG_Element == null) return;
661        Element inlineLogixNG = logixNG_Element.getChild("InlineLogixNG_SystemName");
662        if (inlineLogixNG != null) {
663            String systemName = inlineLogixNG.getTextTrim();
664            p.setLogixNG_SystemName(systemName);
665            InstanceManager.getDefault(LogixNG_Manager.class).registerSetupTask(() -> {
666                p.setupLogixNG();
667            });
668        }
669    }
670
671    private final static Logger log = LoggerFactory.getLogger(PositionableLabelXml.class);
672}