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        if (p.isValueEditDisabled()) {
187            element.setAttribute("valueEditDisabled", "yes");
188        }
189        element.setAttribute("positionable", p.isPositionable() ? "true" : "false");
190        element.setAttribute("showtooltip", p.showToolTip() ? "true" : "false");
191        element.setAttribute("editable", p.isEditable() ? "true" : "false");
192        ToolTip tip = p.getToolTip();
193        if (tip != null) {
194            if (tip.getPrependToolTipWithDisplayName()) {
195                element.addContent(
196                        new Element("tooltip_prependWithDisplayName")
197                                .addContent("yes"));
198            }
199            String txt = tip.getText();
200            if (txt != null) {
201                Element elem = new Element("tooltip").addContent(txt); // was written as "toolTip" 3.5.1 and before
202                element.addContent(elem);
203            }
204        }
205        if (p.getDegrees() != 0) {
206            element.setAttribute("degrees", "" + p.getDegrees());
207        }
208    }
209
210    public Element storeIcon(String elemName, NamedIcon icon) {
211        if (icon == null) {
212            return null;
213        }
214        Element element = new Element(elemName);
215        element.setAttribute("url", icon.getURL());
216        element.setAttribute("degrees", String.valueOf(icon.getDegrees()));
217        element.setAttribute("scale", String.valueOf(icon.getScale()));
218
219        // the "rotate" attribute was deprecated in 2.9.4, replaced by the "rotation" element
220        element.addContent(new Element("rotation").addContent(String.valueOf(icon.getRotation())));
221
222        return element;
223    }
224
225    public void storeLogixNG_Data(Positionable p, Element element) {
226        if (p.getLogixNG() == null) return;
227
228        // Don't save LogixNG data if we don't have any ConditionalNGs
229        if (p.getLogixNG().getNumConditionalNGs() == 0) return;
230        Element logixNG_Element = new Element("LogixNG");
231        logixNG_Element.addContent(new Element("InlineLogixNG_SystemName").addContent(p.getLogixNG().getSystemName()));
232        element.addContent(logixNG_Element);
233    }
234
235    @Override
236    public boolean load(Element shared, Element perNode) {
237        log.error("Invalid method called");
238        return false;
239    }
240
241    /**
242     * Create a PositionableLabel, then add to a target JLayeredPane
243     *
244     * @param element Top level Element to unpack.
245     * @param o       Editor as an Object
246     * @throws JmriConfigureXmlException when a error prevents creating the objects as as
247     *                   required by the input XML
248     */
249    @Override
250    public void load(Element element, Object o) throws JmriConfigureXmlException {
251        // create the objects
252        PositionableLabel l = null;
253
254        // get object class and determine editor being used
255        Editor editor = (Editor) o;
256        if (element.getAttribute("icon") != null) {
257            NamedIcon icon;
258            String name = element.getAttribute("icon").getValue();
259//            if (log.isDebugEnabled()) log.debug("icon attribute= "+name);
260            if (name.equals("yes")) {
261                icon = getNamedIcon("icon", element, "PositionableLabel ", editor);
262            } else {
263                icon = NamedIcon.getIconByName(name);
264                if (icon == null) {
265                    icon = editor.loadFailed("PositionableLabel", name);
266                    if (icon == null) {
267                        log.info("PositionableLabel icon removed for url= {}", name);
268                        return;
269                    }
270                }
271            }
272            // abort if name != yes and have null icon
273            if (icon == null && !name.equals("yes")) {
274                log.info("PositionableLabel icon removed for url= {}", name);
275                return;
276            }
277            l = new PositionableLabel(icon, editor);
278            try {
279                Attribute a = element.getAttribute("rotate");
280                if (a != null && icon != null) {
281                    int rotation = element.getAttribute("rotate").getIntValue();
282                    icon.setRotation(rotation, l);
283                }
284            } catch (org.jdom2.DataConversionException e) {
285            }
286
287            if (name.equals("yes")) {
288                NamedIcon nIcon = loadIcon(l, "icon", element, "PositionableLabel ", editor);
289                if (nIcon != null) {
290                    l.updateIcon(nIcon);
291                } else {
292                    log.info("PositionableLabel icon removed for url= {}", name);
293                    return;
294                }
295            } else {
296                l.updateIcon(icon);
297            }
298        }
299
300        if (element.getAttribute("text") != null) {
301            if (l == null) {
302                l = new PositionableLabel(element.getAttribute("text").getValue(), editor);
303            }
304            loadTextInfo(l, element);
305
306        } else if (l == null) {
307            log.error("PositionableLabel is null!");
308            if (log.isDebugEnabled()) {
309                java.util.List<Attribute> attrs = element.getAttributes();
310                log.debug("\tElement Has {} Attributes:", attrs.size());
311                for (Attribute a : attrs) {
312                    log.debug("  attribute:  {} = {}", a.getName(), a.getValue());
313                }
314                java.util.List<Element> kids = element.getChildren();
315                log.debug("\tElementHas {} children:", kids.size());
316                for (Element e : kids) {
317                    log.debug("  child:  {} = \"{}\"", e.getName(), e.getValue());
318                }
319            }
320            editor.loadFailed();
321            return;
322        }
323        try {
324            editor.putItem(l);
325        } catch (Positionable.DuplicateIdException e) {
326            // This should never happen
327            log.error("Editor.putItem() with null id has thrown DuplicateIdException", e);
328        }
329
330        loadLogixNG_Data(l, element);
331
332        // load individual item's option settings after editor has set its global settings
333        loadCommonAttributes(l, Editor.LABELS, element);
334    }
335
336    protected void loadTextInfo(Positionable l, Element element) {
337        if (log.isDebugEnabled()) {
338            log.debug("loadTextInfo");
339        }
340        jmri.jmrit.display.PositionablePopupUtil util = l.getPopupUtility();
341        if (util == null) {
342            log.warn("PositionablePopupUtil is null! {}", element);
343            return;
344        }
345
346        Attribute a = element.getAttribute("size");
347        try {
348            if (a != null) {
349                util.setFontSize(a.getFloatValue());
350            }
351        } catch (DataConversionException ex) {
352            log.warn("invalid size attribute value");
353        }
354
355        a = element.getAttribute("style");
356        try {
357            if (a != null) {
358                int style = a.getIntValue();
359                int drop = 0;
360                switch (style) {
361                    case 0:  //0 Normal
362                    case 2:  // italic
363                        drop = 1;
364                        break;
365                    default:
366                        break;
367                }
368                util.setFontStyle(style, drop);
369            }
370        } catch (DataConversionException ex) {
371            log.warn("invalid style attribute value");
372        }
373
374        a = element.getAttribute("fontname");
375        try {
376            if (a != null) {
377                util.setFont(new Font(a.getValue(), util.getFontStyle(), util.getFontSize()));
378                // Reset util to the new instance
379                // The setFont process clones the current util instance but the rest of loadTextInfo used the orignal instance.
380                util = l.getPopupUtility();
381            }
382        } catch (NullPointerException e) {  // considered normal if the attributes are not present
383        }
384
385        // set color if needed
386        try {
387            int red = element.getAttribute("red").getIntValue();
388            int blue = element.getAttribute("blue").getIntValue();
389            int green = element.getAttribute("green").getIntValue();
390            util.setForeground(new Color(red, green, blue));
391        } catch (org.jdom2.DataConversionException e) {
392            log.warn("Could not parse color attributes!");
393        } catch (NullPointerException e) {  // considered normal if the attributes are not present
394        }
395
396        a = element.getAttribute("hasBackground");
397        if (a != null) {
398            util.setHasBackground("yes".equals(a.getValue()));
399        } else {
400            util.setHasBackground(true);
401        }
402        if (util.hasBackground()) {
403            try {
404                int red = element.getAttribute("redBack").getIntValue();
405                int blue = element.getAttribute("blueBack").getIntValue();
406                int green = element.getAttribute("greenBack").getIntValue();
407                util.setBackgroundColor(new Color(red, green, blue));
408            } catch (org.jdom2.DataConversionException e) {
409                log.warn("Could not parse background color attributes!");
410            } catch (NullPointerException e) {
411                util.setHasBackground(false);// if the attributes are not listed, we consider the background as clear.
412            }
413        }
414
415        int fixedWidth = 0;
416        int fixedHeight = 0;
417        try {
418            fixedHeight = element.getAttribute("fixedHeight").getIntValue();
419        } catch (org.jdom2.DataConversionException e) {
420            log.warn("Could not parse fixed Height attributes!");
421        } catch (NullPointerException e) {  // considered normal if the attributes are not present
422        }
423
424        try {
425            fixedWidth = element.getAttribute("fixedWidth").getIntValue();
426        } catch (org.jdom2.DataConversionException e) {
427            log.warn("Could not parse fixed Width attribute!");
428        } catch (NullPointerException e) {  // considered normal if the attributes are not present
429        }
430        if (!(fixedWidth == 0 && fixedHeight == 0)) {
431            util.setFixedSize(fixedWidth, fixedHeight);
432        }
433        if ((util.getFixedWidth() == 0) || (util.getFixedHeight() == 0)) {
434            try {
435                util.setMargin(element.getAttribute("margin").getIntValue());
436            } catch (org.jdom2.DataConversionException e) {
437                log.warn("Could not parse margin attribute!");
438            } catch (NullPointerException e) {  // considered normal if the attributes are not present
439            }
440        }
441        try {
442            util.setBorderSize(element.getAttribute("borderSize").getIntValue());
443            int red = element.getAttribute("redBorder").getIntValue();
444            int blue = element.getAttribute("blueBorder").getIntValue();
445            int green = element.getAttribute("greenBorder").getIntValue();
446            util.setBorderColor(new Color(red, green, blue));
447        } catch (org.jdom2.DataConversionException e) {
448            log.warn("Could not parse border attributes!");
449        } catch (NullPointerException e) {  // considered normal if the attribute not present
450        }
451
452        a = element.getAttribute("justification");
453        if (a != null) {
454            util.setJustification(a.getValue());
455        } else {
456            util.setJustification("left");
457        }
458        a = element.getAttribute("orientation");
459        if (a != null) {
460            util.setOrientation(a.getValue());
461        } else {
462            util.setOrientation("horizontal");
463        }
464
465        int deg = 0;
466        try {
467            a = element.getAttribute("degrees");
468            if (a != null) {
469                deg = a.getIntValue();
470                l.rotate(deg);
471            }
472        } catch (DataConversionException ex) {
473            log.warn("invalid 'degrees' value (non integer)");
474        }
475        if (deg == 0 && util.hasBackground()) {
476            l.setOpaque(true);
477        }
478    }
479
480    public void loadCommonAttributes(Positionable l, int defaultLevel, Element element)
481            throws JmriConfigureXmlException {
482
483        if (element.getAttribute("id") != null) {
484            try {
485                l.setId(element.getAttribute("id").getValue());
486            } catch (Positionable.DuplicateIdException e) {
487                throw new JmriConfigureXmlException("Positionable id is not unique", e);
488            }
489        }
490
491        if (element.getAttribute("classes") != null) {
492            String classes = element.getAttribute("classes").getValue();
493            for (String className : classes.split(",")) {
494                if (!className.isBlank()) {
495                    l.addClass(className);
496                }
497            }
498        }
499
500        try {
501            l.setControlling(!element.getAttribute("forcecontroloff").getBooleanValue());
502        } catch (DataConversionException e1) {
503            log.warn("unable to convert positionable label forcecontroloff attribute");
504        } catch (Exception e) {
505        }
506
507        // find coordinates
508        int x = 0;
509        int y = 0;
510        try {
511            x = element.getAttribute("x").getIntValue();
512            y = element.getAttribute("y").getIntValue();
513        } catch (org.jdom2.DataConversionException e) {
514            log.error("failed to convert positional attribute");
515        }
516        l.setLocation(x, y);
517
518        // find display level
519        int level = defaultLevel;
520        try {
521            level = element.getAttribute("level").getIntValue();
522        } catch (org.jdom2.DataConversionException e) {
523            log.warn("Could not parse level attribute!");
524        } catch (NullPointerException e) {
525            // considered normal if the attribute not present
526        }
527        l.setDisplayLevel(level);
528
529        try {
530            boolean value = element.getAttribute("hidden").getBooleanValue();
531            l.setHidden(value);
532            l.setVisible(!value);
533        } catch (DataConversionException e) {
534            log.warn("unable to convert positionable label hidden attribute");
535        } catch (NullPointerException e) {
536            // considered normal if the attribute not present
537        }
538
539        try {
540            boolean value = element.getAttribute("emptyHidden").getBooleanValue();
541            l.setEmptyHidden(value);
542        } catch (DataConversionException e) {
543            log.warn("unable to convert positionable label emptyHidden attribute");
544        } catch (NullPointerException e) {
545            // considered normal if the attribute not present
546        }
547
548        try {
549            boolean value = element.getAttribute("valueEditDisabled").getBooleanValue();
550            l.setValueEditDisabled(value);
551        } catch (DataConversionException e) {
552            log.warn("unable to convert positionable label valueEditDisabled attribute");
553        } catch (NullPointerException e) {
554            // considered normal if the attribute not present
555        }
556
557        try {
558            l.setPositionable(element.getAttribute("positionable").getBooleanValue());
559        } catch (DataConversionException e) {
560            log.warn("unable to convert positionable label positionable attribute");
561        } catch (NullPointerException e) {
562            // considered normal if the attribute not present
563        }
564        try {
565            l.setShowToolTip(element.getAttribute("showtooltip").getBooleanValue());
566        } catch (DataConversionException e) {
567            log.warn("unable to convert positionable label showtooltip attribute");
568        } catch (NullPointerException e) {
569            // considered normal if the attribute not present
570        }
571        try {
572            l.setEditable(element.getAttribute("editable").getBooleanValue());
573        } catch (DataConversionException e) {
574            log.warn("unable to convert positionable label editable attribute");
575        } catch (NullPointerException e) {
576            // considered normal if the attribute not present
577        }
578
579        Attribute a = element.getAttribute("degrees");
580        if (a != null && l instanceof PositionableLabel) {
581            try {
582                int deg = a.getIntValue();
583                ((PositionableLabel) l).setDegrees(deg);
584            } catch (org.jdom2.DataConversionException dce) {
585            }
586        }
587
588        Element elem = element.getChild("tooltip_prependWithDisplayName");
589        if (elem != null) {
590            ToolTip tip = l.getToolTip();
591            if (tip != null) {
592                tip.setPrependToolTipWithDisplayName("yes".equals(elem.getText()));
593            }
594        }
595
596        elem = element.getChild("tooltip");
597        if (elem == null) {
598            elem = element.getChild("toolTip"); // pre JMRI 3.5.2
599        }
600        if (elem != null) {
601            ToolTip tip = l.getToolTip();
602            if (tip != null) {
603                tip.setText(elem.getText());
604            }
605        }
606    }
607
608    public NamedIcon loadIcon(PositionableLabel l, String attrName, Element element,
609            String name, Editor ed) {
610        NamedIcon icon = getNamedIcon(attrName, element, name, ed);
611        if (icon != null) {
612            try {
613                int deg = 0;
614                double scale = 1.0;
615                Element elem = element.getChild(attrName);
616                if (elem != null) {
617                    Attribute a = elem.getAttribute("degrees");
618                    if (a != null) {
619                        deg = a.getIntValue();
620                    }
621                    a = elem.getAttribute("scale");
622                    if (a != null) {
623                        scale = elem.getAttribute("scale").getDoubleValue();
624                    }
625                    icon.setLoad(deg, scale, l);
626                    if (deg == 0) {
627                        // "rotate" attribute is JMRI 2.9.3 and before
628                        a = elem.getAttribute("rotate");
629                        if (a != null) {
630                            int rotation = a.getIntValue();
631                            // 2.9.3 and before, only unscaled icons rotate
632                            if (scale == 1.0) {
633                                icon.setRotation(rotation, l);
634                            }
635                        }
636                        // "rotation" element is JMRI 2.9.4 and after
637                        Element e = elem.getChild("rotation");
638                        if (e != null) {
639                            // ver 2.9.4 allows orthogonal rotations of scaled icons
640                            int rotation = Integer.parseInt(e.getText());
641                            icon.setRotation(rotation, l);
642                        }
643                    }
644                }
645            } catch (org.jdom2.DataConversionException dce) {
646            }
647        }
648        return icon;
649    }
650
651    protected NamedIcon getNamedIcon(String childName, Element element,
652            String name, Editor ed) {
653        NamedIcon icon = null;
654        Element elem = element.getChild(childName);
655        if (elem != null) {
656            String iconName = elem.getAttribute("url").getValue();
657            icon = NamedIcon.getIconByName(iconName);
658            if (icon == null) {
659                icon = ed.loadFailed(name, iconName);
660                if (icon == null) {
661                    log.info("{} removed for url= {}", name, iconName);
662                }
663            }
664        } else {
665            log.debug("getNamedIcon: child element \"{}\" not found in element {}", childName, element.getName());
666        }
667        return icon;
668    }
669
670    public void loadLogixNG_Data(Positionable p, Element element) {
671        Element logixNG_Element = element.getChild("LogixNG");
672        if (logixNG_Element == null) return;
673        Element inlineLogixNG = logixNG_Element.getChild("InlineLogixNG_SystemName");
674        if (inlineLogixNG != null) {
675            String systemName = inlineLogixNG.getTextTrim();
676            p.setLogixNG_SystemName(systemName);
677            InstanceManager.getDefault(LogixNG_Manager.class).registerSetupTask(() -> {
678                p.setupLogixNG();
679            });
680        }
681    }
682
683    private final static Logger log = LoggerFactory.getLogger(PositionableLabelXml.class);
684}