Home | History | Annotate | Download | only in gle2
      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