| /* |
| * 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); |
| } |
| } |