aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java
blob: a1363ecb1771b1d263c793028defdead1a1939d3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
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);
    }
}