1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2; 18 19 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE; 20 21 import com.android.SdkConstants; 22 import com.android.annotations.Nullable; 23 import com.android.ide.common.api.Rect; 24 import com.android.ide.common.rendering.api.IImageFactory; 25 26 import org.eclipse.swt.SWT; 27 import org.eclipse.swt.SWTException; 28 import org.eclipse.swt.graphics.Device; 29 import org.eclipse.swt.graphics.GC; 30 import org.eclipse.swt.graphics.Image; 31 import org.eclipse.swt.graphics.ImageData; 32 import org.eclipse.swt.graphics.PaletteData; 33 34 import java.awt.image.BufferedImage; 35 import java.awt.image.DataBufferInt; 36 import java.awt.image.WritableRaster; 37 import java.lang.ref.SoftReference; 38 39 /** 40 * The {@link ImageOverlay} class renders an image as an overlay. 41 */ 42 public class ImageOverlay extends Overlay implements IImageFactory { 43 /** 44 * Whether the image should be pre-scaled (scaled to the zoom level) once 45 * instead of dynamically during each paint; this is necessary on some 46 * platforms (see issue #19447) 47 */ 48 private static final boolean PRESCALE = 49 // Currently this is necessary on Linux because the "Cairo" library 50 // seems to be a bottleneck 51 SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX 52 && !(Boolean.getBoolean("adt.noprescale")); //$NON-NLS-1$ 53 54 /** Current background image. Null when there's no image. */ 55 private Image mImage; 56 57 /** A pre-scaled version of the image */ 58 private Image mPreScaledImage; 59 60 /** Whether the rendered image should have a drop shadow */ 61 private boolean mShowDropShadow; 62 63 /** Current background AWT image. This is created by {@link #getImage()}, which is called 64 * by the LayoutLib. */ 65 private SoftReference<BufferedImage> mAwtImage = new SoftReference<BufferedImage>(null); 66 67 /** 68 * Strong reference to the image in the above soft reference, to prevent 69 * garbage collection when {@link PRESCALE} is set, until the scaled image 70 * is created (lazily as part of the next paint call, where this strong 71 * reference is nulled out and the above soft reference becomes eligible to 72 * be reclaimed when memory is low.) 73 */ 74 @SuppressWarnings("unused") // Used by the garbage collector to keep mAwtImage non-soft 75 private BufferedImage mAwtImageStrongRef; 76 77 /** The associated {@link LayoutCanvas}. */ 78 private LayoutCanvas mCanvas; 79 80 /** Vertical scaling & scrollbar information. */ 81 private CanvasTransform mVScale; 82 83 /** Horizontal scaling & scrollbar information. */ 84 private CanvasTransform mHScale; 85 86 /** 87 * Constructs an {@link ImageOverlay} tied to the given canvas. 88 * 89 * @param canvas The {@link LayoutCanvas} to paint the overlay over. 90 * @param hScale The horizontal scale information. 91 * @param vScale The vertical scale information. 92 */ 93 public ImageOverlay(LayoutCanvas canvas, CanvasTransform hScale, CanvasTransform vScale) { 94 mCanvas = canvas; 95 mHScale = hScale; 96 mVScale = vScale; 97 } 98 99 @Override 100 public void create(Device device) { 101 super.create(device); 102 } 103 104 @Override 105 public void dispose() { 106 if (mImage != null) { 107 mImage.dispose(); 108 mImage = null; 109 } 110 if (mPreScaledImage != null) { 111 mPreScaledImage.dispose(); 112 mPreScaledImage = null; 113 } 114 } 115 116 /** 117 * Sets the image to be drawn as an overlay from the passed in AWT 118 * {@link BufferedImage} (which will be converted to an SWT image). 119 * <p/> 120 * The image <b>can</b> be null, which is the case when we are dealing with 121 * an empty document. 122 * 123 * @param awtImage The AWT image to be rendered as an SWT image. 124 * @param isAlphaChannelImage whether the alpha channel of the image is relevant 125 * @return The corresponding SWT image, or null. 126 */ 127 public synchronized Image setImage(BufferedImage awtImage, boolean isAlphaChannelImage) { 128 mShowDropShadow = !isAlphaChannelImage; 129 130 BufferedImage oldAwtImage = mAwtImage.get(); 131 if (awtImage != oldAwtImage || awtImage == null) { 132 mAwtImage.clear(); 133 mAwtImageStrongRef = null; 134 135 if (mImage != null) { 136 mImage.dispose(); 137 } 138 139 if (awtImage == null) { 140 mImage = null; 141 } else { 142 mImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), awtImage, 143 isAlphaChannelImage, -1); 144 } 145 } else { 146 assert awtImage instanceof SwtReadyBufferedImage; 147 148 if (isAlphaChannelImage) { 149 if (mImage != null) { 150 mImage.dispose(); 151 } 152 153 mImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), awtImage, true, -1); 154 } else { 155 Image prev = mImage; 156 mImage = ((SwtReadyBufferedImage)awtImage).getSwtImage(); 157 if (prev != mImage && prev != null) { 158 prev.dispose(); 159 } 160 } 161 } 162 163 if (mPreScaledImage != null) { 164 // Force refresh on next paint 165 mPreScaledImage.dispose(); 166 mPreScaledImage = null; 167 } 168 169 return mImage; 170 } 171 172 /** 173 * Returns the currently painted image, or null if none has been set 174 * 175 * @return the currently painted image or null 176 */ 177 public Image getImage() { 178 return mImage; 179 } 180 181 /** 182 * Returns the currently rendered image, or null if none has been set 183 * 184 * @return the currently rendered image or null 185 */ 186 @Nullable 187 BufferedImage getAwtImage() { 188 BufferedImage awtImage = mAwtImage.get(); 189 if (awtImage == null && mImage != null) { 190 awtImage = SwtUtils.convertToAwt(mImage); 191 } 192 193 return awtImage; 194 } 195 196 /** 197 * Returns whether this image overlay should be painted with a drop shadow. 198 * This is usually the case, but not for transparent themes like the dialog 199 * theme (Theme.*Dialog), which already provides its own shadow. 200 * 201 * @return true if the image overlay should be shown with a drop shadow. 202 */ 203 public boolean getShowDropShadow() { 204 return mShowDropShadow; 205 } 206 207 @Override 208 public synchronized void paint(GC gc) { 209 if (mImage != null) { 210 boolean valid = mCanvas.getViewHierarchy().isValid(); 211 mCanvas.ensureZoomed(); 212 if (!valid) { 213 gc_setAlpha(gc, 128); // half-transparent 214 } 215 216 CanvasTransform hi = mHScale; 217 CanvasTransform vi = mVScale; 218 219 // On some platforms, dynamic image scaling is very slow (see issue #19447) so 220 // compute a pre-scaled version of the image once and render that instead. 221 // This is done lazily in paint rather than when the image changes because 222 // the image must be rescaled each time the zoom level changes, which varies 223 // independently from when the image changes. 224 BufferedImage awtImage = mAwtImage.get(); 225 if (PRESCALE && awtImage != null) { 226 int imageWidth = (mPreScaledImage == null) ? 0 227 : mPreScaledImage.getImageData().width 228 - (mShowDropShadow ? SHADOW_SIZE : 0); 229 if (mPreScaledImage == null || imageWidth != hi.getScaledImgSize()) { 230 double xScale = hi.getScaledImgSize() / (double) awtImage.getWidth(); 231 double yScale = vi.getScaledImgSize() / (double) awtImage.getHeight(); 232 BufferedImage scaledAwtImage; 233 234 // NOTE: == comparison on floating point numbers is okay 235 // here because we normalize the scaling factor 236 // to an exact 1.0 in the zooming code when the value gets 237 // near 1.0 to make painting more efficient in the presence 238 // of rounding errors. 239 if (xScale == 1.0 && yScale == 1.0) { 240 // Scaling to 100% is easy! 241 scaledAwtImage = awtImage; 242 243 if (mShowDropShadow) { 244 // Just need to draw drop shadows 245 scaledAwtImage = ImageUtils.createRectangularDropShadow(awtImage); 246 } 247 } else { 248 if (mShowDropShadow) { 249 scaledAwtImage = ImageUtils.scale(awtImage, xScale, yScale, 250 SHADOW_SIZE, SHADOW_SIZE); 251 ImageUtils.drawRectangleShadow(scaledAwtImage, 0, 0, 252 scaledAwtImage.getWidth() - SHADOW_SIZE, 253 scaledAwtImage.getHeight() - SHADOW_SIZE); 254 } else { 255 scaledAwtImage = ImageUtils.scale(awtImage, xScale, yScale); 256 } 257 } 258 259 if (mPreScaledImage != null && !mPreScaledImage.isDisposed()) { 260 mPreScaledImage.dispose(); 261 } 262 mPreScaledImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), scaledAwtImage, 263 true /*transferAlpha*/, -1); 264 // We can't just clear the mAwtImageStrongRef here, because if the 265 // zooming factor changes, we may need to use it again 266 } 267 268 if (mPreScaledImage != null) { 269 gc.drawImage(mPreScaledImage, hi.translate(0), vi.translate(0)); 270 } 271 return; 272 } 273 274 // we only anti-alias when reducing the image size. 275 int oldAlias = -2; 276 if (hi.getScale() < 1.0) { 277 oldAlias = gc_setAntialias(gc, SWT.ON); 278 } 279 280 int srcX = 0; 281 int srcY = 0; 282 int srcWidth = hi.getImgSize(); 283 int srcHeight = vi.getImgSize(); 284 int destX = hi.translate(0); 285 int destY = vi.translate(0); 286 int destWidth = hi.getScaledImgSize(); 287 int destHeight = vi.getScaledImgSize(); 288 289 gc.drawImage(mImage, 290 srcX, srcY, srcWidth, srcHeight, 291 destX, destY, destWidth, destHeight); 292 293 if (mShowDropShadow) { 294 SwtUtils.drawRectangleShadow(gc, destX, destY, destWidth, destHeight); 295 } 296 297 if (oldAlias != -2) { 298 gc_setAntialias(gc, oldAlias); 299 } 300 301 if (!valid) { 302 gc_setAlpha(gc, 255); // opaque 303 } 304 } 305 } 306 307 /** 308 * Sets the alpha for the given GC. 309 * <p/> 310 * Alpha may not work on all platforms and may fail with an exception, which 311 * is hidden here (false is returned in that case). 312 * 313 * @param gc the GC to change 314 * @param alpha the new alpha, 0 for transparent, 255 for opaque. 315 * @return True if the operation worked, false if it failed with an 316 * exception. 317 * @see GC#setAlpha(int) 318 */ 319 private boolean gc_setAlpha(GC gc, int alpha) { 320 try { 321 gc.setAlpha(alpha); 322 return true; 323 } catch (SWTException e) { 324 return false; 325 } 326 } 327 328 /** 329 * Sets the non-text antialias flag for the given GC. 330 * <p/> 331 * Antialias may not work on all platforms and may fail with an exception, 332 * which is hidden here (-2 is returned in that case). 333 * 334 * @param gc the GC to change 335 * @param alias One of {@link SWT#DEFAULT}, {@link SWT#ON}, {@link SWT#OFF}. 336 * @return The previous aliasing mode if the operation worked, or -2 if it 337 * failed with an exception. 338 * @see GC#setAntialias(int) 339 */ 340 private int gc_setAntialias(GC gc, int alias) { 341 try { 342 int old = gc.getAntialias(); 343 gc.setAntialias(alias); 344 return old; 345 } catch (SWTException e) { 346 return -2; 347 } 348 } 349 350 /** 351 * Custom {@link BufferedImage} class able to convert itself into an SWT {@link Image} 352 * efficiently. 353 * 354 * The BufferedImage also contains an instance of {@link ImageData} that's kept around 355 * and used to create new SWT {@link Image} objects in {@link #getSwtImage()}. 356 * 357 */ 358 private static final class SwtReadyBufferedImage extends BufferedImage { 359 360 private final ImageData mImageData; 361 private final Device mDevice; 362 363 /** 364 * Creates the image with a given model, raster and SWT {@link ImageData} 365 * @param model the color model 366 * @param raster the image raster 367 * @param imageData the SWT image data. 368 * @param device the {@link Device} in which the SWT image will be painted. 369 */ 370 private SwtReadyBufferedImage(int width, int height, ImageData imageData, Device device) { 371 super(width, height, BufferedImage.TYPE_INT_ARGB); 372 mImageData = imageData; 373 mDevice = device; 374 } 375 376 /** 377 * Returns a new {@link Image} object initialized with the content of the BufferedImage. 378 * @return the image object. 379 */ 380 private Image getSwtImage() { 381 // transfer the content of the bufferedImage into the image data. 382 WritableRaster raster = getRaster(); 383 int[] imageDataBuffer = ((DataBufferInt) raster.getDataBuffer()).getData(); 384 385 mImageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0); 386 387 return new Image(mDevice, mImageData); 388 } 389 390 /** 391 * Creates a new {@link SwtReadyBufferedImage}. 392 * @param w the width of the image 393 * @param h the height of the image 394 * @param device the device in which the SWT image will be painted 395 * @return a new {@link SwtReadyBufferedImage} object 396 */ 397 private static SwtReadyBufferedImage createImage(int w, int h, Device device) { 398 // NOTE: We can't make this image bigger to accommodate the drop shadow directly 399 // (such that we could paint one into the image after a layoutlib render) 400 // since this image is in the full resolution of the device, and gets scaled 401 // to fit in the layout editor. This would have the net effect of causing 402 // the drop shadow to get zoomed/scaled along with the scene, making a tiny 403 // drop shadow for tablet layouts, a huge drop shadow for tiny QVGA screens, etc. 404 405 ImageData imageData = new ImageData(w, h, 32, 406 new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF)); 407 408 SwtReadyBufferedImage swtReadyImage = new SwtReadyBufferedImage(w, h, 409 imageData, device); 410 411 return swtReadyImage; 412 } 413 } 414 415 /** 416 * Implementation of {@link IImageFactory#getImage(int, int)}. 417 */ 418 @Override 419 public BufferedImage getImage(int w, int h) { 420 BufferedImage awtImage = mAwtImage.get(); 421 if (awtImage == null || 422 awtImage.getWidth() != w || 423 awtImage.getHeight() != h) { 424 mAwtImage.clear(); 425 awtImage = SwtReadyBufferedImage.createImage(w, h, getDevice()); 426 mAwtImage = new SoftReference<BufferedImage>(awtImage); 427 if (PRESCALE) { 428 mAwtImageStrongRef = awtImage; 429 } 430 } 431 432 return awtImage; 433 } 434 435 /** 436 * Returns the bounds of the current image, or null 437 * 438 * @return the bounds of the current image, or null 439 */ 440 public Rect getImageBounds() { 441 if (mImage == null) { 442 return null; 443 } 444 445 return new Rect(0, 0, mImage.getImageData().width, mImage.getImageData().height); 446 } 447 } 448