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      */
ImageOverlay(LayoutCanvas canvas, CanvasTransform hScale, CanvasTransform vScale)93     public ImageOverlay(LayoutCanvas canvas, CanvasTransform hScale, CanvasTransform vScale) {
94         mCanvas = canvas;
95         mHScale = hScale;
96         mVScale = vScale;
97     }
98 
99     @Override
create(Device device)100     public void create(Device device) {
101         super.create(device);
102     }
103 
104     @Override
dispose()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      */
setImage(BufferedImage awtImage, boolean isAlphaChannelImage)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      */
getImage()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
getAwtImage()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      */
getShowDropShadow()203     public boolean getShowDropShadow() {
204         return mShowDropShadow;
205     }
206 
207     @Override
paint(GC gc)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      */
gc_setAlpha(GC gc, int alpha)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      */
gc_setAntialias(GC gc, int alias)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          */
SwtReadyBufferedImage(int width, int height, ImageData imageData, Device device)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          */
getSwtImage()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          */
createImage(int w, int h, Device device)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
getImage(int w, int h)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      */
getImageBounds()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