Home | History | Annotate | Download | only in gle2
      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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    367     int getY() {
    368         return mY;
    369     }
    370 
    371     /** Determine whether this configuration has a better match in a different layout file */
    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
    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      */
    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. */
    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
    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      */
    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      */
    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      */
    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 */
    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 = new RenderLogger(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 
    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      */
    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 
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
   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 
   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
   1186     public Configuration getConfiguration() {
   1187         return mConfiguration;
   1188     }
   1189 
   1190     // ---- Implements IJobChangeListener ----
   1191 
   1192     @Override
   1193     public void aboutToRun(IJobChangeEvent event) {
   1194     }
   1195 
   1196     @Override
   1197     public void awake(IJobChangeEvent event) {
   1198     }
   1199 
   1200     @Override
   1201     public void done(IJobChangeEvent event) {
   1202         mJob = null;
   1203     }
   1204 
   1205     @Override
   1206     public void running(IJobChangeEvent event) {
   1207     }
   1208 
   1209     @Override
   1210     public void scheduled(IJobChangeEvent event) {
   1211     }
   1212 
   1213     @Override
   1214     public void sleeping(IJobChangeEvent event) {
   1215     }
   1216 
   1217     // ---- Delayed Rendering ----
   1218 
   1219     private final class RenderJob extends UIJob {
   1220         public RenderJob() {
   1221             super("RenderPreview");
   1222             setSystem(true);
   1223             setUser(false);
   1224         }
   1225 
   1226         @Override
   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
   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 {
   1248         public AsyncRenderJob() {
   1249             super("RenderPreview");
   1250             setSystem(true);
   1251             setUser(false);
   1252         }
   1253 
   1254         @Override
   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      */
   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      */
   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
   1306     public ConfigurationDescription getDescription() {
   1307         return mDescription;
   1308     }
   1309 
   1310     @Override
   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