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}