001/* An icon stored in XML.
002
003 Copyright (c) 1999-2016 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026
027 */
028package ptolemy.vergil.icon;
029
030import java.io.Reader;
031import java.io.StringReader;
032import java.lang.reflect.Constructor;
033import java.net.URL;
034import java.util.Iterator;
035
036import diva.canvas.Figure;
037import diva.canvas.toolbox.PaintedFigure;
038import diva.canvas.toolbox.SVGParser;
039import diva.gui.toolbox.FigureIcon;
040import diva.util.java2d.PaintedList;
041import diva.util.xml.XmlDocument;
042import diva.util.xml.XmlElement;
043import diva.util.xml.XmlReader;
044import ptolemy.actor.gui.Configuration;
045import ptolemy.kernel.util.ConfigurableAttribute;
046import ptolemy.kernel.util.IllegalActionException;
047import ptolemy.kernel.util.InternalErrorException;
048import ptolemy.kernel.util.NameDuplicationException;
049import ptolemy.kernel.util.Nameable;
050import ptolemy.kernel.util.NamedObj;
051import ptolemy.kernel.util.Settable;
052import ptolemy.kernel.util.StringAttribute;
053import ptolemy.kernel.util.ValueListener;
054import ptolemy.kernel.util.Workspace;
055
056///////////////////////////////////////////////////////////////////
057//// XMLIcon
058
059/**
060 An icon is a visual representation of an entity. Three such visual
061 representations are supported here.  A background figure is returned
062 by the createBackgroundFigure() method.  This figure is specified by
063 an attribute named "_iconDescription" of the container, if there is one.
064 If there is no such attribute, then a default icon is used.
065 The createFigure() method returns this same background figure, but
066 decorated with a label giving the name of the container, unless the
067 container contains a parameter named "_hideName" with value true.
068 The createIcon() method returns a Swing icon given by an attribute named
069 "_smallIconDescription", if there is one.  If there is no such
070 attribute, then the icon is simply a small representation of the
071 background figure.
072 <p>
073 The XML schema used in the "_iconDescription" and "_smallIconDescription"
074 attributes is SVG (scalable vector graphics), although currently Diva
075 only supports a small subset of SVG.
076
077 @author Steve Neuendorffer, John Reekie, Contributors: Edward A. Lee, Chad Berkley (Kepler)
078 @version $Id$
079 @since Ptolemy II 2.0
080 @Pt.ProposedRating Yellow (neuendor)
081 @Pt.AcceptedRating Red (johnr)
082 */
083public class XMLIcon extends DynamicEditorIcon implements ValueListener {
084    /** Construct an icon in the specified workspace and name.
085     *  This constructor is typically used in conjunction with
086     *  setContainerToBe() and createFigure() to create an icon
087     *  and generate a figure without having to have write access
088     *  to the workspace.
089     *  If the workspace argument is null, then use the default workspace.
090     *  The object is added to the directory of the workspace.
091     *  @see #setContainerToBe(NamedObj)
092     *  Increment the version number of the workspace.
093     *  @param workspace The workspace that will list the attribute.
094     *  @param name The name of this attribute.
095     *  @exception IllegalActionException If the specified name contains
096     *   a period.
097     */
098    public XMLIcon(Workspace workspace, String name)
099            throws IllegalActionException {
100        super(workspace, name);
101
102        try {
103            setName(name);
104        } catch (NameDuplicationException ex) {
105            throw new InternalErrorException(ex);
106        }
107    }
108
109    /** Create a new icon with the given name in the given container.
110     *  By default, the icon contains no graphic objects.
111     *  @param container The container for this attribute.
112     *  @param name The name of this attribute.
113     *  @exception IllegalActionException If thrown by the parent
114     *  class or while setting an attribute.
115     *  @exception NameDuplicationException If the name coincides with
116     *   an attribute already in the container.
117     */
118    public XMLIcon(NamedObj container, String name)
119            throws NameDuplicationException, IllegalActionException {
120        super(container, name);
121        _paintedList = null;
122        _description = null;
123    }
124
125    /**
126     * Instantiate an XMLIcon in a NamedObj.
127     *
128     * <p>This method looks for the _alternateXMLIcon attribute in the
129     * configuration. If it is found, it returns an XMLIcon of the
130     * class found there, if not, it returns an instance of this class.
131     *
132     *  @param container The container for this attribute.
133     *  @param name The name of this attribute.
134     *  @return an instance of the XMLIcon class.
135     *  @exception NameDuplicationException  If an object with
136     *  that name already exists in the container.
137     *  @exception IllegalActionException If the specified name contains
138     *   a period.
139     */
140    public static XMLIcon getXMLIcon(NamedObj container, String name)
141            throws NameDuplicationException, IllegalActionException {
142        try {
143            Class XMLIconClass = _getAlternateXMLIcon();
144            if (XMLIconClass == null) {
145                return new XMLIcon(container, name);
146            }
147
148            Class[] argsClass = new Class[] { NamedObj.class, String.class };
149            Constructor alternateXMLIconConstructor = XMLIconClass
150                    .getConstructor(argsClass);
151            XMLIcon xmlIcon = (XMLIcon) alternateXMLIconConstructor
152                    .newInstance(new Object[] { container, name });
153            return xmlIcon;
154        } catch (Exception ex) {
155            System.out.println(
156                    "Warning: could not instantiate alternate XMLIcon class. "
157                            + "Using default XMLIcon.  : " + ex.getMessage());
158            ex.printStackTrace();
159            return new XMLIcon(container, name);
160        }
161    }
162
163    /**
164     * Instantiate an XMLIcon in a Workspace.
165     *
166     * <p>This method looks for the _alternateXMLIcon attribute in the
167     * configuration. If it is found, it returns an XMLIcon of the
168     * class found there, if not, it returns an instance of this class.
169     *
170     *  @param workspace The workspace that will list the attribute.
171     *  @param name The name of this attribute.
172     *  @return an instance of the XMLIcon class.
173     *  @exception NameDuplicationException  If an object with
174     *  that name already exists in the container.
175     *  @exception IllegalActionException If the specified name contains
176     *   a period.
177     */
178    public static XMLIcon getXMLIcon(Workspace workspace, String name)
179            throws NameDuplicationException, IllegalActionException {
180        try {
181            Class XMLIconClass = _getAlternateXMLIcon();
182            if (XMLIconClass == null) {
183                return new XMLIcon(workspace, name);
184            }
185            // Get the new object and return it
186            Class[] argsClass = new Class[] {
187                    ptolemy.kernel.util.Workspace.class, String.class };
188            Constructor alternateXMLIconConstructor = XMLIconClass
189                    .getConstructor(argsClass);
190            XMLIcon xmlIcon = (XMLIcon) alternateXMLIconConstructor
191                    .newInstance(new Object[] { workspace, name });
192            return xmlIcon;
193        } catch (Exception ex) {
194            System.out.println(
195                    "Warning: could not instantiate alternate XMLIcon class. "
196                            + "Using default XMLIcon.  : " + ex.getMessage());
197            ex.printStackTrace();
198            return new XMLIcon(workspace, name);
199        }
200    }
201
202    ///////////////////////////////////////////////////////////////////
203    ////                         public methods                    ////
204
205    /** Clone the object into the specified workspace. The new object is
206     *  <i>not</i> added to the directory of that workspace (you must do this
207     *  yourself if you want it there).
208     *  The result is an object with no container.
209     *  @param workspace The workspace for the cloned object.
210     *  @exception CloneNotSupportedException Not thrown in this base class
211     *  @return The new Attribute.
212     */
213    @Override
214    public Object clone(Workspace workspace) throws CloneNotSupportedException {
215        XMLIcon newObject = (XMLIcon) super.clone(workspace);
216        newObject._paintedList = null;
217        newObject._description = null;
218        newObject._smallIconDescription = null;
219        return newObject;
220    }
221
222    /** Create a background figure based on this icon.  The background figure
223     *  will be painted with each graphic element that this icon contains.
224     *  @return A figure for this icon.
225     */
226    @Override
227    public Figure createBackgroundFigure() {
228        // Get the description.
229        NamedObj container = (NamedObj) getContainerOrContainerToBe();
230        ConfigurableAttribute description = (ConfigurableAttribute) container
231                .getAttribute("_iconDescription");
232
233        // If the description has changed...
234        if (_description != description) {
235            if (_description != null) {
236                // Remove this as a listener if there
237                // was a previous description.
238                _description.removeValueListener(this);
239            }
240
241            // update the description.
242            _description = description;
243
244            if (_description != null) {
245                // Listen for changes in value to the icon description.
246                _description.addValueListener(this);
247            }
248
249            // clear the caches
250            _recreateFigure();
251        }
252
253        // Update the painted list.
254        _updatePaintedList();
255
256        // If the paintedList is still null, then return the default figure.
257        if (_paintedList == null) {
258            return _createDefaultBackgroundFigure();
259        }
260
261        return new PaintedFigure(_paintedList);
262    }
263
264    /** Create a new Swing icon.  This class looks for an attribute
265     *  called "_smallIconDescription", and if it exists, uses it to
266     *  create the icon.  If it does not exist, then it simply creates
267     *  a small version of the background figure returned by
268     *  createBackgroundFigure().
269     *  @return A new Swing Icon.
270     */
271    @Override
272    public javax.swing.Icon createIcon() {
273        // In this class, we cache the rendered icon, since creating icons from
274        // figures is expensive.
275        if (_iconCache != null) {
276            return _iconCache;
277        }
278
279        // No cached object, so rerender the icon.
280        // Get the description.
281        NamedObj container = (NamedObj) getContainerOrContainerToBe();
282        ConfigurableAttribute description = (ConfigurableAttribute) container
283                .getAttribute("_smallIconDescription");
284
285        // If there is no separate small icon description, return
286        // a scaled version of the background figure, as done by the base
287        // class.
288        if (description == null) {
289            return super.createIcon();
290        }
291
292        // If the description has changed...
293        if (_smallIconDescription != description) {
294            if (_smallIconDescription != null) {
295                // Remove this as a listener if there
296                // was a previous description.
297                _smallIconDescription.removeValueListener(this);
298            }
299
300            _smallIconDescription = description;
301
302            // Listen for changes in value to the icon description.
303            _smallIconDescription.addValueListener(this);
304        }
305
306        // clear the caches
307        _recreateFigure();
308
309        // Update the painted list, if necessary
310        Figure figure;
311
312        try {
313            String text = _smallIconDescription.value();
314            Reader in = new StringReader(text);
315
316            // NOTE: Do we need a base here?
317            XmlDocument document = new XmlDocument((URL) null);
318            XmlReader reader = new XmlReader();
319            reader.parse(document, in);
320
321            XmlElement root = document.getRoot();
322            PaintedList paintedList = SVGParser.createPaintedList(root);
323            figure = new PaintedFigure(paintedList);
324        } catch (Exception ex) {
325            return super.createIcon();
326        }
327
328        // NOTE: The size is hardwired here.  Should it be?
329        // The second to last argument specifies the border.
330        // The last says to turn anti-aliasing on.
331        _iconCache = new FigureIcon(figure, 20, 15, 0, true);
332        return _iconCache;
333    }
334
335    /** Return the painted list contained by this icon.
336     *  This is used by the icon editor.
337     *  @return The painted list contained by this icon.
338     */
339    public PaintedList paintedList() {
340        if (_paintedList == null) {
341            _updatePaintedList();
342        }
343
344        return _paintedList;
345    }
346
347    /** Return a string representing this Icon.
348     */
349    @Override
350    public String toString() {
351        String str = super.toString() + "(";
352
353        // FIXME: Something is missing here.
354        return str + ")";
355    }
356
357    /** React to the fact that the value of an attribute named
358     *  "_iconDescription" contained by the same container has changed
359     *  value by redrawing the figure.
360     *  @param settable The object that has changed value.
361     */
362    @Override
363    public void valueChanged(Settable settable) {
364        String name = ((Nameable) settable).getName();
365
366        if (name.equals("_iconDescription")
367                || name.equals("_smallIconDescription")) {
368            _recreateFigure();
369        }
370    }
371
372    ///////////////////////////////////////////////////////////////////
373    ////                         protected methods                 ////
374
375    /** Return a description of the object.  Lines are indented according to
376     *  to the level argument using the protected method _getIndentPrefix().
377     *  Zero, one or two brackets can be specified to surround the returned
378     *  description.  If one is specified it is the the leading bracket.
379     *  This is used by derived classes that will append to the description.
380     *  Those derived classes are responsible for the closing bracket.
381     *  An argument other than 0, 1, or 2 is taken to be equivalent to 0.
382     *  This method is read-synchronized on the workspace.
383     *  @param detail The level of detail.
384     *  @param indent The amount of indenting.
385     *  @param bracket The number of surrounding brackets (0, 1, or 2).
386     *  @return A description of the object.
387     * @exception IllegalActionException
388     */
389    @Override
390    protected String _description(int detail, int indent, int bracket)
391            throws IllegalActionException {
392        String result = "";
393
394        if (bracket == 0) {
395            result += super._description(detail, indent, 0);
396        } else {
397            result += super._description(detail, indent, 1);
398        }
399
400        result += " graphics {\n";
401        result += "FIXME";
402        result += _getIndentPrefix(indent) + "}";
403
404        if (bracket == 2) {
405            result += "}";
406        }
407
408        return result;
409    }
410
411    /** Recreate the figure.  Call to cause createIcon() to call
412     *  createBackgroundFigure() to obtain a new figure.
413     */
414    @Override
415    protected void _recreateFigure() {
416        super._recreateFigure();
417        _paintedList = null;
418    }
419
420    ///////////////////////////////////////////////////////////////////
421    ////                         private methods                   ////
422
423    /**
424     * Check to see if there is an _alternateXMLIcon attribute in the
425     * configuration.  This attribute should name a class that
426     * is an alternative to XMLIcon.
427     * If the attribute is present, return the class, if not, return null.
428     */
429    private static Class _getAlternateXMLIcon() throws Exception {
430        // Applets might not have a configuration, so we check to see
431        // if there is a next configuration.
432        Iterator configurations = Configuration.configurations().iterator();
433        // FIXME: why does this always skip the first configuration?
434        // Instead, we should check each configuration as we go.
435        if (configurations.hasNext()) {
436            Configuration config = (Configuration) configurations.next();
437            String alternateXMLIconClassName = null;
438            if (config != null) {
439                // If _alternateXMLIcon is set in the config, use that
440                // class as the XMLIcon instead of the default
441
442                StringAttribute alternateXMLIconAttribute = (StringAttribute) config
443                        .getAttribute("_alternateXMLIcon");
444                if (alternateXMLIconAttribute != null) {
445                    alternateXMLIconClassName = alternateXMLIconAttribute
446                            .getExpression();
447                }
448
449                if (alternateXMLIconClassName == null) { //attribute was not found
450                    return null;
451                } else {
452                    Class alternateXMLIconClass = Class
453                            .forName(alternateXMLIconClassName);
454                    return alternateXMLIconClass;
455                }
456            }
457        }
458        return null;
459    }
460
461    /** Update the painted list of the icon based on the SVG data
462     *  in the associated "_iconDescription" parameter, if there is one.
463     */
464    private void _updatePaintedList() {
465        // create a new list because the PaintedList we had before
466        // was used to create some PaintedFigures already.
467        // FIXME: test for 'svg' processing instruction
468        if (_description == null) {
469            _paintedList = null;
470            return;
471        }
472
473        try {
474            String text = _description.value();
475            Reader in = new StringReader(text);
476
477            // FIXME: Do we need a base here?
478            XmlDocument document = new XmlDocument((URL) null);
479            XmlReader reader = new XmlReader();
480            reader.parse(document, in);
481
482            XmlElement root = document.getRoot();
483
484            _paintedList = SVGParser.createPaintedList(root);
485        } catch (Exception ex) {
486            // If we fail, then we'll just get a default figure.
487            _paintedList = null;
488        }
489    }
490
491    ///////////////////////////////////////////////////////////////////
492    ////                         private members                   ////
493    // The list of painted objects contained in this icon.
494    private PaintedList _paintedList;
495
496    // The description of this icon in XML.
497    private ConfigurableAttribute _description;
498
499    // The description of the small version of the icon in XML.
500    private ConfigurableAttribute _smallIconDescription;
501}