blob: a1363ecb1771b1d263c793028defdead1a1939d3 [file] [log] [blame]
/*
* 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);
}
}