/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE; import com.android.SdkConstants; import com.android.annotations.Nullable; import com.android.ide.common.api.Rect; import com.android.ide.common.rendering.api.IImageFactory; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.graphics.Device; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.PaletteData; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.awt.image.WritableRaster; import java.lang.ref.SoftReference; /** * The {@link ImageOverlay} class renders an image as an overlay. */ public class ImageOverlay extends Overlay implements IImageFactory { /** * Whether the image should be pre-scaled (scaled to the zoom level) once * instead of dynamically during each paint; this is necessary on some * platforms (see issue #19447) */ private static final boolean PRESCALE = // Currently this is necessary on Linux because the "Cairo" library // seems to be a bottleneck SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX && !(Boolean.getBoolean("adt.noprescale")); //$NON-NLS-1$ /** Current background image. Null when there's no image. */ private Image mImage; /** A pre-scaled version of the image */ private Image mPreScaledImage; /** Whether the rendered image should have a drop shadow */ private boolean mShowDropShadow; /** Current background AWT image. This is created by {@link #getImage()}, which is called * by the LayoutLib. */ private SoftReference<BufferedImage> mAwtImage = new SoftReference<BufferedImage>(null); /** * Strong reference to the image in the above soft reference, to prevent * garbage collection when {@link PRESCALE} is set, until the scaled image * is created (lazily as part of the next paint call, where this strong * reference is nulled out and the above soft reference becomes eligible to * be reclaimed when memory is low.) */ @SuppressWarnings("unused") // Used by the garbage collector to keep mAwtImage non-soft private BufferedImage mAwtImageStrongRef; /** The associated {@link LayoutCanvas}. */ private LayoutCanvas mCanvas; /** Vertical scaling & scrollbar information. */ private CanvasTransform mVScale; /** Horizontal scaling & scrollbar information. */ private CanvasTransform mHScale; /** * Constructs an {@link ImageOverlay} tied to the given canvas. * * @param canvas The {@link LayoutCanvas} to paint the overlay over. * @param hScale The horizontal scale information. * @param vScale The vertical scale information. */ public ImageOverlay(LayoutCanvas canvas, CanvasTransform hScale, CanvasTransform vScale) { mCanvas = canvas; mHScale = hScale; mVScale = vScale; } @Override public void create(Device device) { super.create(device); } @Override public void dispose() { if (mImage != null) { mImage.dispose(); mImage = null; } if (mPreScaledImage != null) { mPreScaledImage.dispose(); mPreScaledImage = null; } } /** * Sets the image to be drawn as an overlay from the passed in AWT * {@link BufferedImage} (which will be converted to an SWT image). * <p/> * The image <b>can</b> be null, which is the case when we are dealing with * an empty document. * * @param awtImage The AWT image to be rendered as an SWT image. * @param isAlphaChannelImage whether the alpha channel of the image is relevant * @return The corresponding SWT image, or null. */ public synchronized Image setImage(BufferedImage awtImage, boolean isAlphaChannelImage) { mShowDropShadow = !isAlphaChannelImage; BufferedImage oldAwtImage = mAwtImage.get(); if (awtImage != oldAwtImage || awtImage == null) { mAwtImage.clear(); mAwtImageStrongRef = null; if (mImage != null) { mImage.dispose(); } if (awtImage == null) { mImage = null; } else { mImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), awtImage, isAlphaChannelImage, -1); } } else { assert awtImage instanceof SwtReadyBufferedImage; if (isAlphaChannelImage) { if (mImage != null) { mImage.dispose(); } mImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), awtImage, true, -1); } else { Image prev = mImage; mImage = ((SwtReadyBufferedImage)awtImage).getSwtImage(); if (prev != mImage && prev != null) { prev.dispose(); } } } if (mPreScaledImage != null) { // Force refresh on next paint mPreScaledImage.dispose(); mPreScaledImage = null; } return mImage; } /** * Returns the currently painted image, or null if none has been set * * @return the currently painted image or null */ public Image getImage() { return mImage; } /** * Returns the currently rendered image, or null if none has been set * * @return the currently rendered image or null */ @Nullable BufferedImage getAwtImage() { BufferedImage awtImage = mAwtImage.get(); if (awtImage == null && mImage != null) { awtImage = SwtUtils.convertToAwt(mImage); } return awtImage; } /** * Returns whether this image overlay should be painted with a drop shadow. * This is usually the case, but not for transparent themes like the dialog * theme (Theme.*Dialog), which already provides its own shadow. * * @return true if the image overlay should be shown with a drop shadow. */ public boolean getShowDropShadow() { return mShowDropShadow; } @Override public synchronized void paint(GC gc) { if (mImage != null) { boolean valid = mCanvas.getViewHierarchy().isValid(); mCanvas.ensureZoomed(); if (!valid) { gc_setAlpha(gc, 128); // half-transparent } CanvasTransform hi = mHScale; CanvasTransform vi = mVScale; // On some platforms, dynamic image scaling is very slow (see issue #19447) so // compute a pre-scaled version of the image once and render that instead. // This is done lazily in paint rather than when the image changes because // the image must be rescaled each time the zoom level changes, which varies // independently from when the image changes. BufferedImage awtImage = mAwtImage.get(); if (PRESCALE && awtImage != null) { int imageWidth = (mPreScaledImage == null) ? 0 : mPreScaledImage.getImageData().width - (mShowDropShadow ? SHADOW_SIZE : 0); if (mPreScaledImage == null || imageWidth != hi.getScaledImgSize()) { double xScale = hi.getScaledImgSize() / (double) awtImage.getWidth(); double yScale = vi.getScaledImgSize() / (double) awtImage.getHeight(); BufferedImage scaledAwtImage; // NOTE: == comparison on floating point numbers is okay // here because we normalize the scaling factor // to an exact 1.0 in the zooming code when the value gets // near 1.0 to make painting more efficient in the presence // of rounding errors. if (xScale == 1.0 && yScale == 1.0) { // Scaling to 100% is easy! scaledAwtImage = awtImage; if (mShowDropShadow) { // Just need to draw drop shadows scaledAwtImage = ImageUtils.createRectangularDropShadow(awtImage); } } else { if (mShowDropShadow) { scaledAwtImage = ImageUtils.scale(awtImage, xScale, yScale, SHADOW_SIZE, SHADOW_SIZE); ImageUtils.drawRectangleShadow(scaledAwtImage, 0, 0, scaledAwtImage.getWidth() - SHADOW_SIZE, scaledAwtImage.getHeight() - SHADOW_SIZE); } else { scaledAwtImage = ImageUtils.scale(awtImage, xScale, yScale); } } if (mPreScaledImage != null && !mPreScaledImage.isDisposed()) { mPreScaledImage.dispose(); } mPreScaledImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), scaledAwtImage, true /*transferAlpha*/, -1); // We can't just clear the mAwtImageStrongRef here, because if the // zooming factor changes, we may need to use it again } if (mPreScaledImage != null) { gc.drawImage(mPreScaledImage, hi.translate(0), vi.translate(0)); } return; } // we only anti-alias when reducing the image size. int oldAlias = -2; if (hi.getScale() < 1.0) { oldAlias = gc_setAntialias(gc, SWT.ON); } int srcX = 0; int srcY = 0; int srcWidth = hi.getImgSize(); int srcHeight = vi.getImgSize(); int destX = hi.translate(0); int destY = vi.translate(0); int destWidth = hi.getScaledImgSize(); int destHeight = vi.getScaledImgSize(); gc.drawImage(mImage, srcX, srcY, srcWidth, srcHeight, destX, destY, destWidth, destHeight); if (mShowDropShadow) { SwtUtils.drawRectangleShadow(gc, destX, destY, destWidth, destHeight); } if (oldAlias != -2) { gc_setAntialias(gc, oldAlias); } if (!valid) { gc_setAlpha(gc, 255); // opaque } } } /** * Sets the alpha for the given GC. * <p/> * Alpha may not work on all platforms and may fail with an exception, which * is hidden here (false is returned in that case). * * @param gc the GC to change * @param alpha the new alpha, 0 for transparent, 255 for opaque. * @return True if the operation worked, false if it failed with an * exception. * @see GC#setAlpha(int) */ private boolean gc_setAlpha(GC gc, int alpha) { try { gc.setAlpha(alpha); return true; } catch (SWTException e) { return false; } } /** * Sets the non-text antialias flag for the given GC. * <p/> * Antialias may not work on all platforms and may fail with an exception, * which is hidden here (-2 is returned in that case). * * @param gc the GC to change * @param alias One of {@link SWT#DEFAULT}, {@link SWT#ON}, {@link SWT#OFF}. * @return The previous aliasing mode if the operation worked, or -2 if it * failed with an exception. * @see GC#setAntialias(int) */ private int gc_setAntialias(GC gc, int alias) { try { int old = gc.getAntialias(); gc.setAntialias(alias); return old; } catch (SWTException e) { return -2; } } /** * Custom {@link BufferedImage} class able to convert itself into an SWT {@link Image} * efficiently. * * The BufferedImage also contains an instance of {@link ImageData} that's kept around * and used to create new SWT {@link Image} objects in {@link #getSwtImage()}. * */ private static final class SwtReadyBufferedImage extends BufferedImage { private final ImageData mImageData; private final Device mDevice; /** * Creates the image with a given model, raster and SWT {@link ImageData} * @param model the color model * @param raster the image raster * @param imageData the SWT image data. * @param device the {@link Device} in which the SWT image will be painted. */ private SwtReadyBufferedImage(int width, int height, ImageData imageData, Device device) { super(width, height, BufferedImage.TYPE_INT_ARGB); mImageData = imageData; mDevice = device; } /** * Returns a new {@link Image} object initialized with the content of the BufferedImage. * @return the image object. */ private Image getSwtImage() { // transfer the content of the bufferedImage into the image data. WritableRaster raster = getRaster(); int[] imageDataBuffer = ((DataBufferInt) raster.getDataBuffer()).getData(); mImageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0); return new Image(mDevice, mImageData); } /** * Creates a new {@link SwtReadyBufferedImage}. * @param w the width of the image * @param h the height of the image * @param device the device in which the SWT image will be painted * @return a new {@link SwtReadyBufferedImage} object */ private static SwtReadyBufferedImage createImage(int w, int h, Device device) { // NOTE: We can't make this image bigger to accommodate the drop shadow directly // (such that we could paint one into the image after a layoutlib render) // since this image is in the full resolution of the device, and gets scaled // to fit in the layout editor. This would have the net effect of causing // the drop shadow to get zoomed/scaled along with the scene, making a tiny // drop shadow for tablet layouts, a huge drop shadow for tiny QVGA screens, etc. ImageData imageData = new ImageData(w, h, 32, new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF)); SwtReadyBufferedImage swtReadyImage = new SwtReadyBufferedImage(w, h, imageData, device); return swtReadyImage; } } /** * Implementation of {@link IImageFactory#getImage(int, int)}. */ @Override public BufferedImage getImage(int w, int h) { BufferedImage awtImage = mAwtImage.get(); if (awtImage == null || awtImage.getWidth() != w || awtImage.getHeight() != h) { mAwtImage.clear(); awtImage = SwtReadyBufferedImage.createImage(w, h, getDevice()); mAwtImage = new SoftReference<BufferedImage>(awtImage); if (PRESCALE) { mAwtImageStrongRef = awtImage; } } return awtImage; } /** * Returns the bounds of the current image, or null * * @return the bounds of the current image, or null */ public Rect getImageBounds() { if (mImage == null) { return null; } return new Rect(0, 0, mImage.getImageData().width, mImage.getImageData().height); } }