001/* An icon that displays a specified java.awt.Image.
002
003 Copyright (c) 2003-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.awt.Image;
031import java.awt.Toolkit;
032import java.awt.image.ImageObserver;
033import java.io.IOException;
034import java.net.URL;
035import java.util.Iterator;
036
037import javax.swing.SwingUtilities;
038
039import diva.canvas.Figure;
040import diva.canvas.toolbox.BasicRectangle;
041import diva.canvas.toolbox.ImageFigure;
042import diva.gui.toolbox.FigureIcon;
043import ptolemy.gui.Top;
044import ptolemy.kernel.util.ChangeRequest;
045import ptolemy.kernel.util.IllegalActionException;
046import ptolemy.kernel.util.NameDuplicationException;
047import ptolemy.kernel.util.NamedObj;
048import ptolemy.kernel.util.Workspace;
049import ptolemy.util.FileUtilities;
050
051///////////////////////////////////////////////////////////////////
052//// ImageIcon
053
054/**
055 An icon that displays a specified java.awt.Image.
056
057 @author Edward A. Lee
058 @version $Id$
059 @since Ptolemy II 4.0
060 @Pt.ProposedRating Yellow (eal)
061 @Pt.AcceptedRating Red (johnr)
062 */
063public class ImageIcon extends DynamicEditorIcon implements ImageObserver {
064    /** Create a new icon with the given name in the given container.
065     *  @param container The container.
066     *  @param name The name of the attribute.
067     *  @exception IllegalActionException If the attribute is not of an
068     *   acceptable class for the container.
069     *  @exception NameDuplicationException If the name coincides with
070     *   an attribute already in the container.
071     */
072    public ImageIcon(NamedObj container, String name)
073            throws IllegalActionException, NameDuplicationException {
074        super(container, name);
075    }
076
077    ///////////////////////////////////////////////////////////////////
078    ////                         public methods                    ////
079
080    /** Clone the object into the specified workspace. The new object is
081     *  <i>not</i> added to the directory of that workspace (you must do this
082     *  yourself if you want it there).
083     *  The result is an object with no container.
084     *  @param workspace The workspace for the cloned object.
085     *  @exception CloneNotSupportedException Not thrown in this base class
086     *  @return The new Attribute.
087     */
088    @Override
089    public Object clone(Workspace workspace) throws CloneNotSupportedException {
090        ImageIcon newObject = (ImageIcon) super.clone(workspace);
091        newObject._image = null;
092        newObject._scaledImage = null;
093        newObject._scalePercentage = 0.0;
094        newObject._scalePercentageImplemented = -1.0;
095        return newObject;
096    }
097
098    /** Create a new default background figure, which is scaled image,
099     *  if it has been set, or a default image if not.
100     *  This must be called in the Swing thread, or a concurrent
101     *  modification exception could occur.
102     *  @return A figure representing the specified shape.
103     */
104    @Override
105    public Figure createBackgroundFigure() {
106        // NOTE: This gets called every time that the graph gets
107        // repainted, which seems excessive to me.  This will happen
108        // every time there is a modification to the model that is
109        // carried out by a ChangeRequest.
110        // The Diva graph package implements a model-view-controller
111        // architecture, which implies that this needs to return a new
112        // figure each time it is called.  The reason is that the figure
113        // may go into a different view, and transformations may be applied
114        // to that figure in that view.  However, this class needs to be
115        // able to update that figure when setShape() is called.  Hence,
116        // this class keeps a list of all the figures it has created.
117        // The references to these figures, however, have to be weak
118        // references, so that this class does not interfere with garbage
119        // collection of the figure when the view is destroyed.
120        Toolkit tk = Toolkit.getDefaultToolkit();
121        if (_scaledImage == null) {
122            // NOTE: This default has to be an ImageFigure, since it
123            // will later have its image set. Create a default image.
124            try {
125                // Use nameToURL so this works in WebStart.
126                URL url = FileUtilities.nameToURL(
127                        "$CLASSPATH/ptolemy/vergil/icon/PtolemyIISmall.gif",
128                        null, getClass().getClassLoader());
129                _scaledImage = _image = tk.getImage(url);
130                setImage(_scaledImage);
131                tk.prepareImage(_scaledImage, -1, -1, this);
132            } catch (IOException ex) {
133                // Ignore, we can't find the icon.
134            }
135        }
136
137        ImageFigure newFigure = null;
138        // Make sure the image is fully loaded before we create the
139        // images. This prevents flashing.
140        if (_scalePercentage == _scalePercentageImplemented
141                && (tk.checkImage(_scaledImage, 43, 33, this)
142                        & ImageObserver.ALLBITS) != 0) {
143            // Current image is fully loaded.
144            newFigure = new ImageFigure(_scaledImage);
145        } else {
146            // If the image is not fully loaded, use an empty
147            // image. The image will be set in the imageUpdate() method.
148            newFigure = new ImageFigure(null);
149        }
150        newFigure.setCentered(false);
151        // Record the figure so that the image can be updated
152        // if it is changed or scaled.
153        _addLiveFigure(newFigure);
154
155        return newFigure;
156    }
157
158    /** Create a new Swing icon. This overrides the base class to
159     *  wait until image has been rendered. Otherwise, we get a null
160     *  pointer exception in Diva, and also the library collapses
161     *  and has to be re-opened.
162     *  @return A new Swing Icon.
163     */
164    @Override
165    public javax.swing.Icon createIcon() {
166        if (_scalePercentage == _scalePercentageImplemented) {
167            // Image processing is done.
168            return super.createIcon();
169        }
170        // Provide a placeholder, but do not store it in
171        // the icon cache.
172        return new FigureIcon(_PLACEHOLDER_ICON, 20, 15);
173    }
174
175    /** This method, which is required by the ImageObserver interface,
176     *  is called if something has changed in a background loading of
177     *  the image.
178     *  @param image The image being observed.
179     *  @param infoflags The bitwise inclusive OR of the following flags:
180     *   WIDTH, HEIGHT, PROPERTIES, SOMEBITS, FRAMEBITS, ALLBITS, ERROR,
181     *   ABORT.
182     *  @param x The x coordinate of the image.
183     *  @param y The y coordinate of the image.
184     *  @param width The width of the image.
185     *  @param height The height of the image.
186     *  @return False if the infoflags indicate that the image is
187     *   completely loaded; true otherwise.
188     */
189    @Override
190    public synchronized boolean imageUpdate(final Image image,
191            final int infoflags, final int x, final int y, final int width,
192            final int height) {
193        // This has to run in the swing event thread.
194        Runnable action = new Runnable() {
195            @Override
196            public void run() {
197                if ((infoflags & ImageObserver.ALLBITS) != 0) {
198                    // The image is now fully loaded.
199                    if (_scalePercentage != 0.0
200                            && _scalePercentage != _scalePercentageImplemented) {
201                        // Scaling has been deferred until the original image
202                        // was fully rendered.  Start the scaling operation again.
203                        scaleImage(_scalePercentage);
204                        // Nothing more to be done on this image.
205                        return;
206                    }
207                    // Either the image passed in is already the scaled image,
208                    // or the scaling has already been implemented.
209                    _updateFigures();
210                    return;
211                }
212
213                if ((infoflags
214                        & (ImageObserver.ERROR | ImageObserver.ABORT)) != 0) {
215                    URL url = getClass().getClassLoader()
216                            .getResource("diva/canvas/toolbox/errorImage.gif");
217                    Toolkit tk = Toolkit.getDefaultToolkit();
218                    Image errorImage = tk.getImage(url);
219                    synchronized (this) {
220                        _image = errorImage;
221                        _scaledImage = errorImage;
222                    }
223                    // Further updates will be needed when the above image
224                    // is updated. To ensure the updates are called, do this:
225                    if (tk.prepareImage(_image, -1, -1, ImageIcon.this)) {
226                        // Image has been fully prepared. Request a re-rendering.
227                        _updateFigures();
228                    }
229                    return;
230                }
231            }
232        };
233        Top.deferIfNecessary(action);
234
235        if ((infoflags & ImageObserver.ALLBITS) != 0) {
236            // The image is now fully loaded.
237            return false;
238        }
239        if ((infoflags & (ImageObserver.ERROR | ImageObserver.ABORT)) != 0) {
240            return true;
241        }
242        // Image is neither complete nor in error.
243        // Needed to trigger further updates.
244        return true;
245    }
246
247    /** Specify a scaling for the image as a percentage.
248     *  @param percentage The scaling percentage.
249     */
250    public synchronized void scaleImage(double percentage) {
251
252        // Record the new scale, even if we can't implement it now.
253        _scalePercentage = percentage;
254        _scalePercentageImplemented = -1.0;
255
256        if (_image == null) {
257            // No image has been set yet, so return.
258            return;
259        }
260
261        // Wait for the original image to be fully rendered, so we
262        // can get its size, then create a new scaled image and set
263        // the images of any Figures that have already been created.
264        // This needs to be in the swing thread.
265        Runnable doScale = new Runnable() {
266            @Override
267            public void run() {
268                synchronized (ImageIcon.this) {
269                    Toolkit tk = Toolkit.getDefaultToolkit();
270                    // NOTE: Oddly, the following two calls below may not
271                    // return the correct sizes unless the image is
272                    // already loaded. Since we are waiting above
273                    // until it is fully loaded, we should be OK.
274                    int width = _image.getWidth(ImageIcon.this);
275                    int height = _image.getHeight(ImageIcon.this);
276                    if (width < 0 || height < 0) {
277                        // Original image is not fully loaded. Wait until it is.
278                        // This will be handled in imageUpdate().
279                        return;
280                    }
281                    int newWidth = (int) Math
282                            .round(width * _scalePercentage / 100.0);
283                    int newHeight = (int) Math
284                            .round(height * _scalePercentage / 100.0);
285
286                    if (newWidth != 0 && newHeight != 0) {
287                        // Avoid "Exception in thread "AWT-EventQueue-0" java.lang.IllegalArgumentException: Width (0) and height (0) must be non-zero"
288                        // which is thrown by java.awt.Image.getScaledInstance() when the height or width is 0.
289                        // This occurs when running
290                        // $PTII/bin/ptcg -language java $PTII/ptolemy/moml/filter/test/auto/modulation2.xml
291                        // Negative argument indicates to maintain aspect ratio.
292                        _scaledImage = _image.getScaledInstance(newWidth, -1,
293                                Image.SCALE_SMOOTH);
294
295                        _scalePercentageImplemented = _scalePercentage;
296
297                        if (tk.prepareImage(_scaledImage, width, height,
298                                ImageIcon.this)) {
299                            // Image is fully prepared. Request a re-rendering.
300                            _updateFigures();
301                        }
302                    }
303                }
304            }
305        };
306
307        SwingUtilities.invokeLater(doScale);
308    }
309
310    /** Specify an image to display. Note that this
311     *  does not actually result in the image displaying.
312     *  You must call scaleImage().
313     *  @param image The image to display.
314     */
315    public synchronized void setImage(Image image) {
316        _image = image;
317        _scaledImage = image;
318
319        // scaleImage() may have been called before this,
320        // in which case it would have done nothing because
321        // _image was null.
322        if (_scalePercentage != _scalePercentageImplemented) {
323            // Delegate to scaleImage().
324            scaleImage(_scalePercentage);
325            return;
326        }
327    }
328
329    ///////////////////////////////////////////////////////////////////
330    ////                         private methods                   ////
331
332    /** Update any previously rendered Diva figures that contain
333     *  this image, and request a re-rendering.
334     */
335    private void _updateFigures() {
336        // If the figure has been previously rendered, first update
337        // the ImageFigure to use the new image.
338        synchronized (_figures) {
339            Iterator figures = _liveFigureIterator();
340            while (figures.hasNext()) {
341                Object figure = figures.next();
342                ((ImageFigure) figure).setImage(_scaledImage);
343            }
344        }
345
346        ChangeRequest request = new ChangeRequest(this,
347                "Dummy change request") {
348            @Override
349            protected void _execute() {
350            }
351        };
352        // Prevent save() being triggered on close just because of this.
353        request.setPersistent(false);
354        requestChange(request);
355    }
356
357    ///////////////////////////////////////////////////////////////////
358    ////                         private variables                 ////
359
360    // The image that is the master.
361    private Image _image;
362
363    // Placeholder icon to be used if images are not fully processed.
364    private static Figure _PLACEHOLDER_ICON = new BasicRectangle(0.0, 0.0, 10.0,
365            10.0);
366
367    // The scaled version of the image that is the master.
368    private Image _scaledImage;
369
370    // The scale percentage. 0.0 means unspecified.
371    private double _scalePercentage = 0.0;
372
373    // The scale percentage that has been implemented.
374    // 0.0 means that the specified percentage has not been implemented.
375    private double _scalePercentageImplemented = -1.0;
376}