diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java | 447 |
1 files changed, 447 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java new file mode 100644 index 000000000..a1363ecb1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java @@ -0,0 +1,447 @@ +/* + * 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); + } +} |