1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
17 
18 import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
19 import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
20 import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
21 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_RENDERING;
22 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE;
23 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SMALL_SHADOW_SIZE;
24 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.DEFAULT;
25 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.INCLUDES;
26 
27 import com.android.annotations.NonNull;
28 import com.android.annotations.Nullable;
29 import com.android.ide.common.rendering.api.RenderSession;
30 import com.android.ide.common.rendering.api.ResourceValue;
31 import com.android.ide.common.rendering.api.Result;
32 import com.android.ide.common.rendering.api.Result.Status;
33 import com.android.ide.common.resources.ResourceFile;
34 import com.android.ide.common.resources.ResourceRepository;
35 import com.android.ide.common.resources.ResourceResolver;
36 import com.android.ide.common.resources.configuration.FolderConfiguration;
37 import com.android.ide.common.resources.configuration.ScreenOrientationQualifier;
38 import com.android.ide.eclipse.adt.AdtPlugin;
39 import com.android.ide.eclipse.adt.AdtUtils;
40 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
41 import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
42 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
43 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
44 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient;
45 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription;
46 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Locale;
47 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.NestedConfiguration;
48 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.VaryingConfiguration;
49 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
50 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
51 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
52 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
53 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
54 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
55 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
56 import com.android.ide.eclipse.adt.io.IFileWrapper;
57 import com.android.io.IAbstractFile;
58 import com.android.resources.Density;
59 import com.android.resources.ResourceType;
60 import com.android.resources.ScreenOrientation;
61 import com.android.sdklib.IAndroidTarget;
62 import com.android.sdklib.devices.Device;
63 import com.android.sdklib.devices.Screen;
64 import com.android.sdklib.devices.State;
65 import com.android.utils.SdkUtils;
66 
67 import org.eclipse.core.resources.IFile;
68 import org.eclipse.core.runtime.IProgressMonitor;
69 import org.eclipse.core.runtime.IStatus;
70 import org.eclipse.core.runtime.jobs.IJobChangeEvent;
71 import org.eclipse.core.runtime.jobs.IJobChangeListener;
72 import org.eclipse.core.runtime.jobs.Job;
73 import org.eclipse.jface.dialogs.InputDialog;
74 import org.eclipse.jface.window.Window;
75 import org.eclipse.swt.SWT;
76 import org.eclipse.swt.graphics.Color;
77 import org.eclipse.swt.graphics.GC;
78 import org.eclipse.swt.graphics.Image;
79 import org.eclipse.swt.graphics.ImageData;
80 import org.eclipse.swt.graphics.Point;
81 import org.eclipse.swt.graphics.Region;
82 import org.eclipse.swt.widgets.Display;
83 import org.eclipse.ui.ISharedImages;
84 import org.eclipse.ui.PlatformUI;
85 import org.eclipse.ui.progress.UIJob;
86 import org.w3c.dom.Document;
87 
88 import java.awt.Graphics2D;
89 import java.awt.image.BufferedImage;
90 import java.io.File;
91 import java.lang.ref.SoftReference;
92 import java.util.Comparator;
93 import java.util.Map;
94 
95 /**
96  * Represents a preview rendering of a given configuration
97  */
98 public class RenderPreview implements IJobChangeListener {
99     /** Whether previews should use large shadows */
100     static final boolean LARGE_SHADOWS = false;
101 
102     /**
103      * Still doesn't work; get exceptions from layoutlib:
104      * java.lang.IllegalStateException: After scene creation, #init() must be called
105      *   at com.android.layoutlib.bridge.impl.RenderAction.acquire(RenderAction.java:151)
106      * <p>
107      * TODO: Investigate.
108      */
109     private static final boolean RENDER_ASYNC = false;
110 
111     /**
112      * Height of the toolbar shown over a preview during hover. Needs to be
113      * large enough to accommodate icons below.
114      */
115     private static final int HEADER_HEIGHT = 20;
116 
117     /** Whether to dump out rendering failures of the previews to the log */
118     private static final boolean DUMP_RENDER_DIAGNOSTICS = false;
119 
120     /** Extra error checking in debug mode */
121     private static final boolean DEBUG = false;
122 
123     private static final Image EDIT_ICON;
124     private static final Image ZOOM_IN_ICON;
125     private static final Image ZOOM_OUT_ICON;
126     private static final Image CLOSE_ICON;
127     private static final int EDIT_ICON_WIDTH;
128     private static final int ZOOM_IN_ICON_WIDTH;
129     private static final int ZOOM_OUT_ICON_WIDTH;
130     private static final int CLOSE_ICON_WIDTH;
131     static {
132         ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
133         IconFactory icons = IconFactory.getInstance();
134         CLOSE_ICON = sharedImages.getImage(ISharedImages.IMG_ETOOL_DELETE);
135         EDIT_ICON = icons.getIcon("editPreview");   //$NON-NLS-1$
136         ZOOM_IN_ICON = icons.getIcon("zoomplus");   //$NON-NLS-1$
137         ZOOM_OUT_ICON = icons.getIcon("zoomminus"); //$NON-NLS-1$
138         CLOSE_ICON_WIDTH = CLOSE_ICON.getImageData().width;
139         EDIT_ICON_WIDTH = EDIT_ICON.getImageData().width;
140         ZOOM_IN_ICON_WIDTH = ZOOM_IN_ICON.getImageData().width;
141         ZOOM_OUT_ICON_WIDTH = ZOOM_OUT_ICON.getImageData().width;
142     }
143 
144     /** The configuration being previewed */
145     private @NonNull Configuration mConfiguration;
146 
147     /** Configuration to use if we have an alternate input to be rendered */
148     private @NonNull Configuration mAlternateConfiguration;
149 
150     /** The associated manager */
151     private final @NonNull RenderPreviewManager mManager;
152     private final @NonNull LayoutCanvas mCanvas;
153 
154     private @NonNull SoftReference<ResourceResolver> mResourceResolver =
155             new SoftReference<ResourceResolver>(null);
156     private @Nullable Job mJob;
157     private @Nullable Image mThumbnail;
158     private @Nullable String mDisplayName;
159     private int mWidth;
160     private int mHeight;
161     private int mX;
162     private int mY;
163     private int mTitleHeight;
164     private double mScale = 1.0;
165     private double mAspectRatio;
166 
167     /** If non null, points to a separate file containing the source */
168     private @Nullable IFile mAlternateInput;
169 
170     /** If included within another layout, the name of that outer layout */
171     private @Nullable Reference mIncludedWithin;
172 
173     /** Whether the mouse is actively hovering over this preview */
174     private boolean mActive;
175 
176     /**
177      * Whether this preview cannot be rendered because of a model error - such
178      * as an invalid configuration, a missing resource, an error in the XML
179      * markup, etc. If non null, contains the error message (or a blank string
180      * if not known), and null if the render was successful.
181      */
182     private String mError;
183 
184     /** Whether in the current layout, this preview is visible */
185     private boolean mVisible;
186 
187     /** Whether the configuration has changed and needs to be refreshed the next time
188      * this preview made visible. This corresponds to the change flags in
189      * {@link ConfigurationClient}. */
190     private int mDirty;
191 
192     /**
193      * Creates a new {@linkplain RenderPreview}
194      *
195      * @param manager the manager
196      * @param canvas canvas where preview is painted
197      * @param configuration the associated configuration
198      * @param width the initial width to use for the preview
199      * @param height the initial height to use for the preview
200      */
RenderPreview( @onNull RenderPreviewManager manager, @NonNull LayoutCanvas canvas, @NonNull Configuration configuration)201     private RenderPreview(
202             @NonNull RenderPreviewManager manager,
203             @NonNull LayoutCanvas canvas,
204             @NonNull Configuration configuration) {
205         mManager = manager;
206         mCanvas = canvas;
207         mConfiguration = configuration;
208         updateSize();
209 
210         // Should only attempt to create configurations for fully configured devices
211         assert mConfiguration.getDevice() != null
212                 && mConfiguration.getDeviceState() != null
213                 && mConfiguration.getLocale() != null
214                 && mConfiguration.getTarget() != null
215                 && mConfiguration.getTheme() != null
216                 && mConfiguration.getFullConfig() != null
217                 && mConfiguration.getFullConfig().getScreenSizeQualifier() != null :
218                     mConfiguration;
219     }
220 
221     /**
222      * Sets the configuration to use for this preview
223      *
224      * @param configuration the new configuration
225      */
setConfiguration(@onNull Configuration configuration)226     public void setConfiguration(@NonNull Configuration configuration) {
227         mConfiguration = configuration;
228     }
229 
230     /**
231      * Gets the scale being applied to the thumbnail
232      *
233      * @return the scale being applied to the thumbnail
234      */
getScale()235     public double getScale() {
236         return mScale;
237     }
238 
239     /**
240      * Sets the scale to apply to the thumbnail
241      *
242      * @param scale the factor to scale the thumbnail picture by
243      */
setScale(double scale)244     public void setScale(double scale) {
245         disposeThumbnail();
246         mScale = scale;
247     }
248 
249     /**
250      * Returns the aspect ratio of this render preview
251      *
252      * @return the aspect ratio
253      */
getAspectRatio()254     public double getAspectRatio() {
255         return mAspectRatio;
256     }
257 
258     /**
259      * Returns whether the preview is actively hovered
260      *
261      * @return whether the mouse is hovering over the preview
262      */
isActive()263     public boolean isActive() {
264         return mActive;
265     }
266 
267     /**
268      * Sets whether the preview is actively hovered
269      *
270      * @param active if the mouse is hovering over the preview
271      */
setActive(boolean active)272     public void setActive(boolean active) {
273         mActive = active;
274     }
275 
276     /**
277      * Returns whether the preview is visible. Previews that are off
278      * screen are typically marked invisible during layout, which means we don't
279      * have to expend effort computing preview thumbnails etc
280      *
281      * @return true if the preview is visible
282      */
isVisible()283     public boolean isVisible() {
284         return mVisible;
285     }
286 
287     /**
288      * Returns whether this preview represents a forked layout
289      *
290      * @return true if this preview represents a separate file
291      */
isForked()292     public boolean isForked() {
293         return mAlternateInput != null || mIncludedWithin != null;
294     }
295 
296     /**
297      * Returns the file to be used for this preview, or null if this is not a
298      * forked layout meaning that the file is the one used in the chooser
299      *
300      * @return the file or null for non-forked layouts
301      */
302     @Nullable
getAlternateInput()303     public IFile getAlternateInput() {
304         if (mAlternateInput != null) {
305             return mAlternateInput;
306         } else if (mIncludedWithin != null) {
307             return mIncludedWithin.getFile();
308         }
309 
310         return null;
311     }
312 
313     /**
314      * Returns the area of this render preview, PRIOR to scaling
315      *
316      * @return the area (width times height without scaling)
317      */
getArea()318     int getArea() {
319         return mWidth * mHeight;
320     }
321 
322     /**
323      * Sets whether the preview is visible. Previews that are off
324      * screen are typically marked invisible during layout, which means we don't
325      * have to expend effort computing preview thumbnails etc
326      *
327      * @param visible whether this preview is visible
328      */
setVisible(boolean visible)329     public void setVisible(boolean visible) {
330         if (visible != mVisible) {
331             mVisible = visible;
332             if (mVisible) {
333                 if (mDirty != 0) {
334                     // Just made the render preview visible:
335                     configurationChanged(mDirty); // schedules render
336                 } else {
337                     updateForkStatus();
338                     mManager.scheduleRender(this);
339                 }
340             } else {
341                 dispose();
342             }
343         }
344     }
345 
346     /**
347      * Sets the layout position relative to the top left corner of the preview
348      * area, in control coordinates
349      */
setPosition(int x, int y)350     void setPosition(int x, int y) {
351         mX = x;
352         mY = y;
353     }
354 
355     /**
356      * Gets the layout X position relative to the top left corner of the preview
357      * area, in control coordinates
358      */
getX()359     int getX() {
360         return mX;
361     }
362 
363     /**
364      * Gets the layout Y position relative to the top left corner of the preview
365      * area, in control coordinates
366      */
getY()367     int getY() {
368         return mY;
369     }
370 
371     /** Determine whether this configuration has a better match in a different layout file */
updateForkStatus()372     private void updateForkStatus() {
373         ConfigurationChooser chooser = mManager.getChooser();
374         FolderConfiguration config = mConfiguration.getFullConfig();
375         if (mAlternateInput != null && chooser.isBestMatchFor(mAlternateInput, config)) {
376             return;
377         }
378 
379         mAlternateInput = null;
380         IFile editedFile = chooser.getEditedFile();
381         if (editedFile != null) {
382             if (!chooser.isBestMatchFor(editedFile, config)) {
383                 ProjectResources resources = chooser.getResources();
384                 if (resources != null) {
385                     ResourceFile best = resources.getMatchingFile(editedFile.getName(),
386                             ResourceType.LAYOUT, config);
387                     if (best != null) {
388                         IAbstractFile file = best.getFile();
389                         if (file instanceof IFileWrapper) {
390                             mAlternateInput = ((IFileWrapper) file).getIFile();
391                         } else if (file instanceof File) {
392                             mAlternateInput = AdtUtils.fileToIFile(((File) file));
393                         }
394                     }
395                 }
396                 if (mAlternateInput != null) {
397                     mAlternateConfiguration = Configuration.create(mConfiguration,
398                             mAlternateInput);
399                 }
400             }
401         }
402     }
403 
404     /**
405      * Creates a new {@linkplain RenderPreview}
406      *
407      * @param manager the manager
408      * @param configuration the associated configuration
409      * @return a new configuration
410      */
411     @NonNull
create( @onNull RenderPreviewManager manager, @NonNull Configuration configuration)412     public static RenderPreview create(
413             @NonNull RenderPreviewManager manager,
414             @NonNull Configuration configuration) {
415         LayoutCanvas canvas = manager.getCanvas();
416         return new RenderPreview(manager, canvas, configuration);
417     }
418 
419     /**
420      * Throws away this preview: cancels any pending rendering jobs and disposes
421      * of image resources etc
422      */
dispose()423     public void dispose() {
424         disposeThumbnail();
425 
426         if (mJob != null) {
427             mJob.cancel();
428             mJob = null;
429         }
430     }
431 
432     /** Disposes the thumbnail rendering. */
disposeThumbnail()433     void disposeThumbnail() {
434         if (mThumbnail != null) {
435             mThumbnail.dispose();
436             mThumbnail = null;
437         }
438     }
439 
440     /**
441      * Returns the display name of this preview
442      *
443      * @return the name of the preview
444      */
445     @NonNull
getDisplayName()446     public String getDisplayName() {
447         if (mDisplayName == null) {
448             String displayName = getConfiguration().getDisplayName();
449             if (displayName == null) {
450                 // No display name: this must be the configuration used by default
451                 // for the view which is originally displayed (before adding thumbnails),
452                 // and you've switched away to something else; now we need to display a name
453                 // for this original configuration. For now, just call it "Original"
454                 return "Original";
455             }
456 
457             return displayName;
458         }
459 
460         return mDisplayName;
461     }
462 
463     /**
464      * Sets the display name of this preview. By default, the display name is
465      * the display name of the configuration, but it can be overridden by calling
466      * this setter (which only sets the preview name, without editing the configuration.)
467      *
468      * @param displayName the new display name
469      */
setDisplayName(@onNull String displayName)470     public void setDisplayName(@NonNull String displayName) {
471         mDisplayName = displayName;
472     }
473 
474     /**
475      * Sets an inclusion context to use for this layout, if any. This will render
476      * the configuration preview as the outer layout with the current layout
477      * embedded within.
478      *
479      * @param includedWithin a reference to a layout which includes this one
480      */
setIncludedWithin(Reference includedWithin)481     public void setIncludedWithin(Reference includedWithin) {
482         mIncludedWithin = includedWithin;
483     }
484 
485     /**
486      * Request a new render after the given delay
487      *
488      * @param delay the delay to wait before starting the render job
489      */
render(long delay)490     public void render(long delay) {
491         Job job = mJob;
492         if (job != null) {
493             job.cancel();
494         }
495         if (RENDER_ASYNC) {
496             job = new AsyncRenderJob();
497         } else {
498             job = new RenderJob();
499         }
500         job.schedule(delay);
501         job.addJobChangeListener(this);
502         mJob = job;
503     }
504 
505     /** Render immediately */
renderSync()506     private void renderSync() {
507         GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor();
508         if (editor.getReadyLayoutLib(false /*displayError*/) == null) {
509             // Don't attempt to render when there is no ready layout library: most likely
510             // the targets are loading/reloading.
511             return;
512         }
513 
514         disposeThumbnail();
515 
516         Configuration configuration =
517                 mAlternateInput != null && mAlternateConfiguration != null
518                 ? mAlternateConfiguration : mConfiguration;
519         ResourceResolver resolver = getResourceResolver(configuration);
520         RenderService renderService = RenderService.create(editor, configuration, resolver);
521 
522         if (mIncludedWithin != null) {
523             renderService.setIncludedWithin(mIncludedWithin);
524         }
525 
526         if (mAlternateInput != null) {
527             IAndroidTarget target = editor.getRenderingTarget();
528             AndroidTargetData data = null;
529             if (target != null) {
530                 Sdk sdk = Sdk.getCurrent();
531                 if (sdk != null) {
532                     data = sdk.getTargetData(target);
533                 }
534             }
535 
536             // Construct UI model from XML
537             DocumentDescriptor documentDescriptor;
538             if (data == null) {
539                 documentDescriptor = new DocumentDescriptor("temp", null);//$NON-NLS-1$
540             } else {
541                 documentDescriptor = data.getLayoutDescriptors().getDescriptor();
542             }
543             UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode();
544             model.setEditor(mCanvas.getEditorDelegate().getEditor());
545             model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider());
546 
547             Document document = DomUtilities.getDocument(mAlternateInput);
548             if (document == null) {
549                 mError = "No document";
550                 createErrorThumbnail();
551                 return;
552             }
553             model.loadFromXmlNode(document);
554             renderService.setModel(model);
555         } else {
556             renderService.setModel(editor.getModel());
557         }
558         RenderLogger log = editor.createRenderLogger(getDisplayName());
559         renderService.setLog(log);
560         RenderSession session = renderService.createRenderSession();
561         Result render = session.render(1000);
562 
563         if (DUMP_RENDER_DIAGNOSTICS) {
564             if (log.hasProblems() || !render.isSuccess()) {
565                 AdtPlugin.log(IStatus.ERROR, "Found problems rendering preview "
566                         + getDisplayName() + ": "
567                         + render.getErrorMessage() + " : "
568                         + log.getProblems(false));
569                 Throwable exception = render.getException();
570                 if (exception != null) {
571                     AdtPlugin.log(exception, "Failure rendering preview " + getDisplayName());
572                 }
573             }
574         }
575 
576         if (render.isSuccess()) {
577             mError = null;
578         } else {
579             mError = render.getErrorMessage();
580             if (mError == null) {
581                 mError = "";
582             }
583         }
584 
585         if (render.getStatus() == Status.ERROR_TIMEOUT) {
586             // TODO: Special handling? schedule update again later
587             return;
588         }
589         if (render.isSuccess()) {
590             BufferedImage image = session.getImage();
591             if (image != null) {
592                 createThumbnail(image);
593             }
594         }
595 
596         if (mError != null) {
597             createErrorThumbnail();
598         }
599     }
600 
getResourceResolver(Configuration configuration)601     private ResourceResolver getResourceResolver(Configuration configuration) {
602         ResourceResolver resourceResolver = mResourceResolver.get();
603         if (resourceResolver != null) {
604             return resourceResolver;
605         }
606 
607         GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor();
608         String theme = configuration.getTheme();
609         if (theme == null) {
610             return null;
611         }
612 
613         Map<ResourceType, Map<String, ResourceValue>> configuredFrameworkRes = null;
614         Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes = null;
615 
616         FolderConfiguration config = configuration.getFullConfig();
617         IAndroidTarget target = graphicalEditor.getRenderingTarget();
618         ResourceRepository frameworkRes = null;
619         if (target != null) {
620             Sdk sdk = Sdk.getCurrent();
621             if (sdk == null) {
622                 return null;
623             }
624             AndroidTargetData data = sdk.getTargetData(target);
625 
626             if (data != null) {
627                 // TODO: SHARE if possible
628                 frameworkRes = data.getFrameworkResources();
629                 configuredFrameworkRes = frameworkRes.getConfiguredResources(config);
630             } else {
631                 return null;
632             }
633         } else {
634             return null;
635         }
636         assert configuredFrameworkRes != null;
637 
638 
639         // get the resources of the file's project.
640         ProjectResources projectRes = ResourceManager.getInstance().getProjectResources(
641                 graphicalEditor.getProject());
642         configuredProjectRes = projectRes.getConfiguredResources(config);
643 
644         if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
645             if (frameworkRes.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + theme)) {
646                 theme = ANDROID_STYLE_RESOURCE_PREFIX + theme;
647             } else {
648                 theme = STYLE_RESOURCE_PREFIX + theme;
649             }
650         }
651 
652         resourceResolver = ResourceResolver.create(
653                 configuredProjectRes, configuredFrameworkRes,
654                 ResourceHelper.styleToTheme(theme),
655                 ResourceHelper.isProjectStyle(theme));
656         mResourceResolver = new SoftReference<ResourceResolver>(resourceResolver);
657         return resourceResolver;
658     }
659 
660     /**
661      * Sets the new image of the preview and generates a thumbnail
662      *
663      * @param image the full size image
664      */
createThumbnail(BufferedImage image)665     void createThumbnail(BufferedImage image) {
666         if (image == null) {
667             mThumbnail = null;
668             return;
669         }
670 
671         ImageOverlay imageOverlay = mCanvas.getImageOverlay();
672         boolean drawShadows = imageOverlay == null || imageOverlay.getShowDropShadow();
673         double scale = getWidth() / (double) image.getWidth();
674         int shadowSize;
675         if (LARGE_SHADOWS) {
676             shadowSize = drawShadows ? SHADOW_SIZE : 0;
677         } else {
678             shadowSize = drawShadows ? SMALL_SHADOW_SIZE : 0;
679         }
680         if (scale < 1.0) {
681             if (LARGE_SHADOWS) {
682                 image = ImageUtils.scale(image, scale, scale,
683                         shadowSize, shadowSize);
684                 if (drawShadows) {
685                     ImageUtils.drawRectangleShadow(image, 0, 0,
686                             image.getWidth() - shadowSize,
687                             image.getHeight() - shadowSize);
688                 }
689             } else {
690                 image = ImageUtils.scale(image, scale, scale,
691                         shadowSize, shadowSize);
692                 if (drawShadows) {
693                     ImageUtils.drawSmallRectangleShadow(image, 0, 0,
694                             image.getWidth() - shadowSize,
695                             image.getHeight() - shadowSize);
696                 }
697             }
698         }
699 
700         mThumbnail = SwtUtils.convertToSwt(mCanvas.getDisplay(), image,
701                 true /* transferAlpha */, -1);
702     }
703 
createErrorThumbnail()704     void createErrorThumbnail() {
705         int shadowSize = LARGE_SHADOWS ? SHADOW_SIZE : SMALL_SHADOW_SIZE;
706         int width = getWidth();
707         int height = getHeight();
708         BufferedImage image = new BufferedImage(width + shadowSize, height + shadowSize,
709                 BufferedImage.TYPE_INT_ARGB);
710 
711         Graphics2D g = image.createGraphics();
712         g.setColor(new java.awt.Color(0xfffbfcc6));
713         g.fillRect(0, 0, width, height);
714 
715         g.dispose();
716 
717         ImageOverlay imageOverlay = mCanvas.getImageOverlay();
718         boolean drawShadows = imageOverlay == null || imageOverlay.getShowDropShadow();
719         if (drawShadows) {
720             if (LARGE_SHADOWS) {
721                 ImageUtils.drawRectangleShadow(image, 0, 0,
722                         image.getWidth() - SHADOW_SIZE,
723                         image.getHeight() - SHADOW_SIZE);
724             } else {
725                 ImageUtils.drawSmallRectangleShadow(image, 0, 0,
726                         image.getWidth() - SMALL_SHADOW_SIZE,
727                         image.getHeight() - SMALL_SHADOW_SIZE);
728             }
729         }
730 
731         mThumbnail = SwtUtils.convertToSwt(mCanvas.getDisplay(), image,
732                 true /* transferAlpha */, -1);
733     }
734 
getScale(int width, int height)735     private static double getScale(int width, int height) {
736         int maxWidth = RenderPreviewManager.getMaxWidth();
737         int maxHeight = RenderPreviewManager.getMaxHeight();
738         if (width > 0 && height > 0
739                 && (width > maxWidth || height > maxHeight)) {
740             if (width >= height) { // landscape
741                 return maxWidth / (double) width;
742             } else { // portrait
743                 return maxHeight / (double) height;
744             }
745         }
746 
747         return 1.0;
748     }
749 
750     /**
751      * Returns the width of the preview, in pixels
752      *
753      * @return the width in pixels
754      */
getWidth()755     public int getWidth() {
756         return (int) (mWidth * mScale * RenderPreviewManager.getScale());
757     }
758 
759     /**
760      * Returns the height of the preview, in pixels
761      *
762      * @return the height in pixels
763      */
getHeight()764     public int getHeight() {
765         return (int) (mHeight * mScale * RenderPreviewManager.getScale());
766     }
767 
768     /**
769      * Handles clicks within the preview (x and y are positions relative within the
770      * preview
771      *
772      * @param x the x coordinate within the preview where the click occurred
773      * @param y the y coordinate within the preview where the click occurred
774      * @return true if this preview handled (and therefore consumed) the click
775      */
click(int x, int y)776     public boolean click(int x, int y) {
777         if (y >= mTitleHeight && y < mTitleHeight + HEADER_HEIGHT) {
778             int left = 0;
779             left += CLOSE_ICON_WIDTH;
780             if (x <= left) {
781                 // Delete
782                 mManager.deletePreview(this);
783                 return true;
784             }
785             left += ZOOM_IN_ICON_WIDTH;
786             if (x <= left) {
787                 // Zoom in
788                 mScale = mScale * (1 / 0.5);
789                 if (Math.abs(mScale-1.0) < 0.0001) {
790                     mScale = 1.0;
791                 }
792 
793                 render(0);
794                 mManager.layout(true);
795                 mCanvas.redraw();
796                 return true;
797             }
798             left += ZOOM_OUT_ICON_WIDTH;
799             if (x <= left) {
800                 // Zoom out
801                 mScale = mScale * (0.5 / 1);
802                 if (Math.abs(mScale-1.0) < 0.0001) {
803                     mScale = 1.0;
804                 }
805                 render(0);
806 
807                 mManager.layout(true);
808                 mCanvas.redraw();
809                 return true;
810             }
811             left += EDIT_ICON_WIDTH;
812             if (x <= left) {
813                 // Edit. For now, just rename
814                 InputDialog d = new InputDialog(
815                         AdtPlugin.getShell(),
816                         "Rename Preview",  // title
817                         "Name:",
818                         getDisplayName(),
819                         null);
820                 if (d.open() == Window.OK) {
821                     String newName = d.getValue();
822                     mConfiguration.setDisplayName(newName);
823                     if (mDescription != null) {
824                         mManager.rename(mDescription, newName);
825                     }
826                     mCanvas.redraw();
827                 }
828 
829                 return true;
830             }
831 
832             // Clicked anywhere else on header
833             // Perhaps open Edit dialog here?
834         }
835 
836         mManager.switchTo(this);
837         return true;
838     }
839 
840     /**
841      * Paints the preview at the given x/y position
842      *
843      * @param gc the graphics context to paint it into
844      * @param x the x coordinate to paint the preview at
845      * @param y the y coordinate to paint the preview at
846      */
paint(GC gc, int x, int y)847     void paint(GC gc, int x, int y) {
848         mTitleHeight = paintTitle(gc, x, y, true /*showFile*/);
849         y += mTitleHeight;
850         y += 2;
851 
852         int width = getWidth();
853         int height = getHeight();
854         if (mThumbnail != null && mError == null) {
855             gc.drawImage(mThumbnail, x, y);
856 
857             if (mActive) {
858                 int oldWidth = gc.getLineWidth();
859                 gc.setLineWidth(3);
860                 gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_LIST_SELECTION));
861                 gc.drawRectangle(x - 1, y - 1, width + 2, height + 2);
862                 gc.setLineWidth(oldWidth);
863             }
864         } else if (mError != null) {
865             if (mThumbnail != null) {
866                 gc.drawImage(mThumbnail, x, y);
867             } else {
868                 gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER));
869                 gc.drawRectangle(x, y, width, height);
870             }
871 
872             gc.setClipping(x, y, width, height);
873             Image icon = IconFactory.getInstance().getIcon("renderError"); //$NON-NLS-1$
874             ImageData data = icon.getImageData();
875             int prevAlpha = gc.getAlpha();
876             int alpha = 96;
877             if (mThumbnail != null) {
878                 alpha -= 32;
879             }
880             gc.setAlpha(alpha);
881             gc.drawImage(icon, x + (width - data.width) / 2, y + (height - data.height) / 2);
882 
883             String msg = mError;
884             Density density = mConfiguration.getDensity();
885             if (density == Density.TV || density == Density.LOW) {
886                 msg = "Broken rendering library; unsupported DPI. Try using the SDK manager " +
887                         "to get updated layout libraries.";
888             }
889             int charWidth = gc.getFontMetrics().getAverageCharWidth();
890             int charsPerLine = (width - 10) / charWidth;
891             msg = SdkUtils.wrap(msg, charsPerLine, null);
892             gc.setAlpha(255);
893             gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_BLACK));
894             gc.drawText(msg, x + 5, y + HEADER_HEIGHT, true);
895             gc.setAlpha(prevAlpha);
896             gc.setClipping((Region) null);
897         } else {
898             gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER));
899             gc.drawRectangle(x, y, width, height);
900 
901             Image icon = IconFactory.getInstance().getIcon("refreshPreview"); //$NON-NLS-1$
902             ImageData data = icon.getImageData();
903             int prevAlpha = gc.getAlpha();
904             gc.setAlpha(96);
905             gc.drawImage(icon, x + (width - data.width) / 2,
906                     y + (height - data.height) / 2);
907             gc.setAlpha(prevAlpha);
908         }
909 
910         if (mActive) {
911             int left = x ;
912             int prevAlpha = gc.getAlpha();
913             gc.setAlpha(208);
914             Color bg = mCanvas.getDisplay().getSystemColor(SWT.COLOR_WHITE);
915             gc.setBackground(bg);
916             gc.fillRectangle(left, y, x + width - left, HEADER_HEIGHT);
917             gc.setAlpha(prevAlpha);
918 
919             y += 2;
920 
921             // Paint icons
922             gc.drawImage(CLOSE_ICON, left, y);
923             left += CLOSE_ICON_WIDTH;
924 
925             gc.drawImage(ZOOM_IN_ICON, left, y);
926             left += ZOOM_IN_ICON_WIDTH;
927 
928             gc.drawImage(ZOOM_OUT_ICON, left, y);
929             left += ZOOM_OUT_ICON_WIDTH;
930 
931             gc.drawImage(EDIT_ICON, left, y);
932             left += EDIT_ICON_WIDTH;
933         }
934     }
935 
936     /**
937      * Paints the preview title at the given position (and returns the required
938      * height)
939      *
940      * @param gc the graphics context to paint into
941      * @param x the left edge of the preview rectangle
942      * @param y the top edge of the preview rectangle
943      */
paintTitle(GC gc, int x, int y, boolean showFile)944     private int paintTitle(GC gc, int x, int y, boolean showFile) {
945         String displayName = getDisplayName();
946         return paintTitle(gc, x, y, showFile, displayName);
947     }
948 
949     /**
950      * Paints the preview title at the given position (and returns the required
951      * height)
952      *
953      * @param gc the graphics context to paint into
954      * @param x the left edge of the preview rectangle
955      * @param y the top edge of the preview rectangle
956      * @param displayName the title string to be used
957      */
paintTitle(GC gc, int x, int y, boolean showFile, String displayName)958     int paintTitle(GC gc, int x, int y, boolean showFile, String displayName) {
959         int titleHeight = 0;
960 
961         if (showFile && mIncludedWithin != null) {
962             if (mManager.getMode() != INCLUDES) {
963                 displayName = "<include>";
964             } else {
965                 // Skip: just paint footer instead
966                 displayName = null;
967             }
968         }
969 
970         int width = getWidth();
971         int labelTop = y + 1;
972         gc.setClipping(x, labelTop, width, 100);
973 
974         // Use font height rather than extent height since we want two adjacent
975         // previews (which may have different display names and therefore end
976         // up with slightly different extent heights) to have identical title
977         // heights such that they are aligned identically
978         int fontHeight = gc.getFontMetrics().getHeight();
979 
980         if (displayName != null && displayName.length() > 0) {
981             gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_WHITE));
982             Point extent = gc.textExtent(displayName);
983             int labelLeft = Math.max(x, x + (width - extent.x) / 2);
984             Image icon = null;
985             Locale locale = mConfiguration.getLocale();
986             if (locale != null && (locale.hasLanguage() || locale.hasRegion())
987                     && (!(mConfiguration instanceof NestedConfiguration)
988                             || ((NestedConfiguration) mConfiguration).isOverridingLocale())) {
989                 icon = locale.getFlagImage();
990             }
991 
992             if (icon != null) {
993                 int flagWidth = icon.getImageData().width;
994                 int flagHeight = icon.getImageData().height;
995                 labelLeft = Math.max(x + flagWidth / 2, labelLeft);
996                 gc.drawImage(icon, labelLeft - flagWidth / 2 - 1, labelTop);
997                 labelLeft += flagWidth / 2 + 1;
998                 gc.drawText(displayName, labelLeft,
999                         labelTop - (extent.y - flagHeight) / 2, true);
1000             } else {
1001                 gc.drawText(displayName, labelLeft, labelTop, true);
1002             }
1003 
1004             labelTop += extent.y;
1005             titleHeight += fontHeight;
1006         }
1007 
1008         if (showFile && (mAlternateInput != null || mIncludedWithin != null)) {
1009             // Draw file flag, and parent folder name
1010             IFile file = mAlternateInput != null
1011                     ? mAlternateInput : mIncludedWithin.getFile();
1012             String fileName = file.getParent().getName() + File.separator
1013                     + file.getName();
1014             Point extent = gc.textExtent(fileName);
1015             Image icon = IconFactory.getInstance().getIcon("android_file"); //$NON-NLS-1$
1016             int flagWidth = icon.getImageData().width;
1017             int flagHeight = icon.getImageData().height;
1018 
1019             int labelLeft = Math.max(x, x + (width - extent.x - flagWidth - 1) / 2);
1020 
1021             gc.drawImage(icon, labelLeft, labelTop);
1022 
1023             gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY));
1024             labelLeft += flagWidth + 1;
1025             labelTop -= (extent.y - flagHeight) / 2;
1026             gc.drawText(fileName, labelLeft, labelTop, true);
1027 
1028             titleHeight += Math.max(titleHeight, icon.getImageData().height);
1029         }
1030 
1031         gc.setClipping((Region) null);
1032 
1033         return titleHeight;
1034     }
1035 
1036     /**
1037      * Notifies that the preview's configuration has changed.
1038      *
1039      * @param flags the change flags, a bitmask corresponding to the
1040      *            {@code CHANGE_} constants in {@link ConfigurationClient}
1041      */
configurationChanged(int flags)1042     public void configurationChanged(int flags) {
1043         if (!mVisible) {
1044             mDirty |= flags;
1045             return;
1046         }
1047 
1048         if ((flags & MASK_RENDERING) != 0) {
1049             mResourceResolver.clear();
1050             // Handle inheritance
1051             mConfiguration.syncFolderConfig();
1052             updateForkStatus();
1053             updateSize();
1054         }
1055 
1056         // Sanity check to make sure things are working correctly
1057         if (DEBUG) {
1058             RenderPreviewMode mode = mManager.getMode();
1059             if (mode == DEFAULT) {
1060                 assert mConfiguration instanceof VaryingConfiguration;
1061                 VaryingConfiguration config = (VaryingConfiguration) mConfiguration;
1062                 int alternateFlags = config.getAlternateFlags();
1063                 switch (alternateFlags) {
1064                     case Configuration.CFG_DEVICE_STATE: {
1065                         State configState = config.getDeviceState();
1066                         State chooserState = mManager.getChooser().getConfiguration()
1067                                 .getDeviceState();
1068                         assert configState != null && chooserState != null;
1069                         assert !configState.getName().equals(chooserState.getName())
1070                                 : configState.toString() + ':' + chooserState;
1071 
1072                         Device configDevice = config.getDevice();
1073                         Device chooserDevice = mManager.getChooser().getConfiguration()
1074                                 .getDevice();
1075                         assert configDevice != null && chooserDevice != null;
1076                         assert configDevice == chooserDevice
1077                                 : configDevice.toString() + ':' + chooserDevice;
1078 
1079                         break;
1080                     }
1081                     case Configuration.CFG_DEVICE: {
1082                         Device configDevice = config.getDevice();
1083                         Device chooserDevice = mManager.getChooser().getConfiguration()
1084                                 .getDevice();
1085                         assert configDevice != null && chooserDevice != null;
1086                         assert configDevice != chooserDevice
1087                                 : configDevice.toString() + ':' + chooserDevice;
1088 
1089                         State configState = config.getDeviceState();
1090                         State chooserState = mManager.getChooser().getConfiguration()
1091                                 .getDeviceState();
1092                         assert configState != null && chooserState != null;
1093                         assert configState.getName().equals(chooserState.getName())
1094                                 : configState.toString() + ':' + chooserState;
1095 
1096                         break;
1097                     }
1098                     case Configuration.CFG_LOCALE: {
1099                         Locale configLocale = config.getLocale();
1100                         Locale chooserLocale = mManager.getChooser().getConfiguration()
1101                                 .getLocale();
1102                         assert configLocale != null && chooserLocale != null;
1103                         assert configLocale != chooserLocale
1104                                 : configLocale.toString() + ':' + chooserLocale;
1105                         break;
1106                     }
1107                     default: {
1108                         // Some other type of override I didn't anticipate
1109                         assert false : alternateFlags;
1110                     }
1111                 }
1112             }
1113         }
1114 
1115         mDirty = 0;
1116         mManager.scheduleRender(this);
1117     }
1118 
updateSize()1119     private void updateSize() {
1120         Device device = mConfiguration.getDevice();
1121         if (device == null) {
1122             return;
1123         }
1124         Screen screen = device.getDefaultHardware().getScreen();
1125         if (screen == null) {
1126             return;
1127         }
1128 
1129         FolderConfiguration folderConfig = mConfiguration.getFullConfig();
1130         ScreenOrientationQualifier qualifier = folderConfig.getScreenOrientationQualifier();
1131         ScreenOrientation orientation = qualifier == null
1132                 ? ScreenOrientation.PORTRAIT : qualifier.getValue();
1133 
1134         // compute width and height to take orientation into account.
1135         int x = screen.getXDimension();
1136         int y = screen.getYDimension();
1137         int screenWidth, screenHeight;
1138 
1139         if (x > y) {
1140             if (orientation == ScreenOrientation.LANDSCAPE) {
1141                 screenWidth = x;
1142                 screenHeight = y;
1143             } else {
1144                 screenWidth = y;
1145                 screenHeight = x;
1146             }
1147         } else {
1148             if (orientation == ScreenOrientation.LANDSCAPE) {
1149                 screenWidth = y;
1150                 screenHeight = x;
1151             } else {
1152                 screenWidth = x;
1153                 screenHeight = y;
1154             }
1155         }
1156 
1157         int width = RenderPreviewManager.getMaxWidth();
1158         int height = RenderPreviewManager.getMaxHeight();
1159         if (screenWidth > 0) {
1160             double scale = getScale(screenWidth, screenHeight);
1161             width = (int) (screenWidth * scale);
1162             height = (int) (screenHeight * scale);
1163         }
1164 
1165         if (width != mWidth || height != mHeight) {
1166             mWidth = width;
1167             mHeight = height;
1168 
1169             Image thumbnail = mThumbnail;
1170             mThumbnail = null;
1171             if (thumbnail != null) {
1172                 thumbnail.dispose();
1173             }
1174             if (mHeight != 0) {
1175                 mAspectRatio = mWidth / (double) mHeight;
1176             }
1177         }
1178     }
1179 
1180     /**
1181      * Returns the configuration associated with this preview
1182      *
1183      * @return the configuration
1184      */
1185     @NonNull
getConfiguration()1186     public Configuration getConfiguration() {
1187         return mConfiguration;
1188     }
1189 
1190     // ---- Implements IJobChangeListener ----
1191 
1192     @Override
aboutToRun(IJobChangeEvent event)1193     public void aboutToRun(IJobChangeEvent event) {
1194     }
1195 
1196     @Override
awake(IJobChangeEvent event)1197     public void awake(IJobChangeEvent event) {
1198     }
1199 
1200     @Override
done(IJobChangeEvent event)1201     public void done(IJobChangeEvent event) {
1202         mJob = null;
1203     }
1204 
1205     @Override
running(IJobChangeEvent event)1206     public void running(IJobChangeEvent event) {
1207     }
1208 
1209     @Override
scheduled(IJobChangeEvent event)1210     public void scheduled(IJobChangeEvent event) {
1211     }
1212 
1213     @Override
sleeping(IJobChangeEvent event)1214     public void sleeping(IJobChangeEvent event) {
1215     }
1216 
1217     // ---- Delayed Rendering ----
1218 
1219     private final class RenderJob extends UIJob {
RenderJob()1220         public RenderJob() {
1221             super("RenderPreview");
1222             setSystem(true);
1223             setUser(false);
1224         }
1225 
1226         @Override
runInUIThread(IProgressMonitor monitor)1227         public IStatus runInUIThread(IProgressMonitor monitor) {
1228             mJob = null;
1229             if (!mCanvas.isDisposed()) {
1230                 renderSync();
1231                 mCanvas.redraw();
1232                 return org.eclipse.core.runtime.Status.OK_STATUS;
1233             }
1234 
1235             return org.eclipse.core.runtime.Status.CANCEL_STATUS;
1236         }
1237 
1238         @Override
getDisplay()1239         public Display getDisplay() {
1240             if (mCanvas.isDisposed()) {
1241                 return null;
1242             }
1243             return mCanvas.getDisplay();
1244         }
1245     }
1246 
1247     private final class AsyncRenderJob extends Job {
AsyncRenderJob()1248         public AsyncRenderJob() {
1249             super("RenderPreview");
1250             setSystem(true);
1251             setUser(false);
1252         }
1253 
1254         @Override
run(IProgressMonitor monitor)1255         protected IStatus run(IProgressMonitor monitor) {
1256             mJob = null;
1257 
1258             if (mCanvas.isDisposed()) {
1259                 return org.eclipse.core.runtime.Status.CANCEL_STATUS;
1260             }
1261 
1262             renderSync();
1263 
1264             // Update display
1265             mCanvas.getDisplay().asyncExec(new Runnable() {
1266                 @Override
1267                 public void run() {
1268                     mCanvas.redraw();
1269                 }
1270             });
1271 
1272             return org.eclipse.core.runtime.Status.OK_STATUS;
1273         }
1274     }
1275 
1276     /**
1277      * Sets the input file to use for rendering. If not set, this will just be
1278      * the same file as the configuration chooser. This is used to render other
1279      * layouts, such as variations of the currently edited layout, which are
1280      * not kept in sync with the main layout.
1281      *
1282      * @param file the file to set as input
1283      */
setAlternateInput(@ullable IFile file)1284     public void setAlternateInput(@Nullable IFile file) {
1285         mAlternateInput = file;
1286     }
1287 
1288     /** Corresponding description for this preview if it is a manually added preview */
1289     private @Nullable ConfigurationDescription mDescription;
1290 
1291     /**
1292      * Sets the description of this preview, if this preview is a manually added preview
1293      *
1294      * @param description the description of this preview
1295      */
setDescription(@ullable ConfigurationDescription description)1296     public void setDescription(@Nullable ConfigurationDescription description) {
1297         mDescription = description;
1298     }
1299 
1300     /**
1301      * Returns the description of this preview, if this preview is a manually added preview
1302      *
1303      * @return the description
1304      */
1305     @Nullable
getDescription()1306     public ConfigurationDescription getDescription() {
1307         return mDescription;
1308     }
1309 
1310     @Override
toString()1311     public String toString() {
1312         return getDisplayName() + ':' + mConfiguration;
1313     }
1314 
1315     /** Sorts render previews into increasing aspect ratio order */
1316     static Comparator<RenderPreview> INCREASING_ASPECT_RATIO = new Comparator<RenderPreview>() {
1317         @Override
1318         public int compare(RenderPreview preview1, RenderPreview preview2) {
1319             return (int) Math.signum(preview1.mAspectRatio - preview2.mAspectRatio);
1320         }
1321     };
1322     /** Sorts render previews into visual order: row by row, column by column */
1323     static Comparator<RenderPreview> VISUAL_ORDER = new Comparator<RenderPreview>() {
1324         @Override
1325         public int compare(RenderPreview preview1, RenderPreview preview2) {
1326             int delta = preview1.mY - preview2.mY;
1327             if (delta == 0) {
1328                 delta = preview1.mX - preview2.mX;
1329             }
1330             return delta;
1331         }
1332     };
1333 }
1334