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 
     17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
     18 
     19 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE;
     20 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE_STATE;
     21 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_ALL;
     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.RenderPreview.LARGE_SHADOWS;
     25 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.CUSTOM;
     26 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.NONE;
     27 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.SCREENS;
     28 
     29 import com.android.annotations.NonNull;
     30 import com.android.annotations.Nullable;
     31 import com.android.ide.common.api.Rect;
     32 import com.android.ide.common.rendering.api.Capability;
     33 import com.android.ide.common.resources.configuration.DensityQualifier;
     34 import com.android.ide.common.resources.configuration.DeviceConfigHelper;
     35 import com.android.ide.common.resources.configuration.FolderConfiguration;
     36 import com.android.ide.common.resources.configuration.LanguageQualifier;
     37 import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
     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.common.CommonXmlEditor;
     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.preferences.AdtPrefs;
     51 import com.android.resources.Density;
     52 import com.android.resources.ScreenSize;
     53 import com.android.sdklib.devices.Device;
     54 import com.android.sdklib.devices.Screen;
     55 import com.android.sdklib.devices.State;
     56 import com.google.common.collect.Lists;
     57 
     58 import org.eclipse.core.resources.IFile;
     59 import org.eclipse.core.resources.IProject;
     60 import org.eclipse.jface.dialogs.InputDialog;
     61 import org.eclipse.jface.window.Window;
     62 import org.eclipse.swt.SWT;
     63 import org.eclipse.swt.events.SelectionEvent;
     64 import org.eclipse.swt.events.SelectionListener;
     65 import org.eclipse.swt.graphics.GC;
     66 import org.eclipse.swt.graphics.Image;
     67 import org.eclipse.swt.graphics.Rectangle;
     68 import org.eclipse.swt.widgets.ScrollBar;
     69 import org.eclipse.ui.IWorkbenchPartSite;
     70 import org.eclipse.ui.PartInitException;
     71 import org.eclipse.ui.ide.IDE;
     72 
     73 import java.io.IOException;
     74 import java.util.ArrayList;
     75 import java.util.Collections;
     76 import java.util.Comparator;
     77 import java.util.HashSet;
     78 import java.util.Iterator;
     79 import java.util.List;
     80 import java.util.Set;
     81 
     82 /**
     83  * Manager for the configuration previews, which handles layout computations,
     84  * managing the image buffer cache, etc
     85  */
     86 public class RenderPreviewManager {
     87     private static double sScale = 1.0;
     88     private static final int RENDER_DELAY = 150;
     89     private static final int PREVIEW_VGAP = 18;
     90     private static final int PREVIEW_HGAP = 12;
     91     private static final int MAX_WIDTH = 200;
     92     private static final int MAX_HEIGHT = MAX_WIDTH;
     93     private static final int ZOOM_ICON_WIDTH = 16;
     94     private static final int ZOOM_ICON_HEIGHT = 16;
     95     private @Nullable List<RenderPreview> mPreviews;
     96     private @Nullable RenderPreviewList mManualList;
     97     private final @NonNull LayoutCanvas mCanvas;
     98     private final @NonNull CanvasTransform mVScale;
     99     private final @NonNull CanvasTransform mHScale;
    100     private int mPrevCanvasWidth;
    101     private int mPrevCanvasHeight;
    102     private int mPrevImageWidth;
    103     private int mPrevImageHeight;
    104     private @NonNull RenderPreviewMode mMode = NONE;
    105     private @Nullable RenderPreview mActivePreview;
    106     private @Nullable ScrollBarListener mListener;
    107     private int mLayoutHeight;
    108     /** Last seen state revision in this {@link RenderPreviewManager}. If less
    109      * than {@link #sRevision}, the previews need to be updated on next exposure */
    110     private static int mRevision;
    111     /** Current global revision count */
    112     private static int sRevision;
    113     private boolean mNeedLayout;
    114     private boolean mNeedRender;
    115     private boolean mNeedZoom;
    116     private SwapAnimation mAnimation;
    117 
    118     /**
    119      * Creates a {@link RenderPreviewManager} associated with the given canvas
    120      *
    121      * @param canvas the canvas to manage previews for
    122      */
    123     public RenderPreviewManager(@NonNull LayoutCanvas canvas) {
    124         mCanvas = canvas;
    125         mHScale = canvas.getHorizontalTransform();
    126         mVScale = canvas.getVerticalTransform();
    127     }
    128 
    129     /**
    130      * Revise the global state revision counter. This will cause all layout
    131      * preview managers to refresh themselves to the latest revision when they
    132      * are next exposed.
    133      */
    134     public static void bumpRevision() {
    135         sRevision++;
    136     }
    137 
    138     /**
    139      * Returns the associated chooser
    140      *
    141      * @return the associated chooser
    142      */
    143     @NonNull
    144     ConfigurationChooser getChooser() {
    145         GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor();
    146         return editor.getConfigurationChooser();
    147     }
    148 
    149     /**
    150      * Returns the associated canvas
    151      *
    152      * @return the canvas
    153      */
    154     @NonNull
    155     public LayoutCanvas getCanvas() {
    156         return mCanvas;
    157     }
    158 
    159     /** Zooms in (grows all previews) */
    160     public void zoomIn() {
    161         sScale = sScale * (1 / 0.9);
    162         if (Math.abs(sScale-1.0) < 0.0001) {
    163             sScale = 1.0;
    164         }
    165 
    166         updatedZoom();
    167     }
    168 
    169     /** Zooms out (shrinks all previews) */
    170     public void zoomOut() {
    171         sScale = sScale * (0.9 / 1);
    172         if (Math.abs(sScale-1.0) < 0.0001) {
    173             sScale = 1.0;
    174         }
    175         updatedZoom();
    176     }
    177 
    178     /** Zooms to 100 (resets zoom) */
    179     public void zoomReset() {
    180         sScale = 1.0;
    181         updatedZoom();
    182         mNeedZoom = mNeedLayout = true;
    183         mCanvas.redraw();
    184     }
    185 
    186     private void updatedZoom() {
    187         if (hasPreviews()) {
    188             for (RenderPreview preview : mPreviews) {
    189                 preview.disposeThumbnail();
    190             }
    191             RenderPreview preview = mCanvas.getPreview();
    192             if (preview != null) {
    193                 preview.disposeThumbnail();
    194             }
    195         }
    196 
    197         mNeedLayout = mNeedRender = true;
    198         mCanvas.redraw();
    199     }
    200 
    201     static int getMaxWidth() {
    202         return (int) (sScale * MAX_WIDTH);
    203     }
    204 
    205     static int getMaxHeight() {
    206         return (int) (sScale * MAX_HEIGHT);
    207     }
    208 
    209     static double getScale() {
    210         return sScale;
    211     }
    212 
    213     /**
    214      * Returns whether there are any manual preview items (provided the current
    215      * mode is manual previews
    216      *
    217      * @return true if there are items in the manual preview list
    218      */
    219     public boolean hasManualPreviews() {
    220         assert mMode == CUSTOM;
    221         return mManualList != null && !mManualList.isEmpty();
    222     }
    223 
    224     /** Delete all the previews */
    225     public void deleteManualPreviews() {
    226         disposePreviews();
    227         selectMode(NONE);
    228         mCanvas.setFitScale(true /* onlyZoomOut */, true /*allowZoomIn*/);
    229 
    230         if (mManualList != null) {
    231             mManualList.delete();
    232         }
    233     }
    234 
    235     /** Dispose all the previews */
    236     public void disposePreviews() {
    237         if (mPreviews != null) {
    238             List<RenderPreview> old = mPreviews;
    239             mPreviews = null;
    240             for (RenderPreview preview : old) {
    241                 preview.dispose();
    242             }
    243         }
    244     }
    245 
    246     /**
    247      * Deletes the given preview
    248      *
    249      * @param preview the preview to be deleted
    250      */
    251     public void deletePreview(RenderPreview preview) {
    252         mPreviews.remove(preview);
    253         preview.dispose();
    254         layout(true);
    255         mCanvas.redraw();
    256 
    257         if (mManualList != null) {
    258             mManualList.remove(preview);
    259             saveList();
    260         }
    261     }
    262 
    263     /**
    264      * Compute the total width required for the previews, including internal padding
    265      *
    266      * @return total width in pixels
    267      */
    268     public int computePreviewWidth() {
    269         int maxPreviewWidth = 0;
    270         if (hasPreviews()) {
    271             for (RenderPreview preview : mPreviews) {
    272                 maxPreviewWidth = Math.max(maxPreviewWidth, preview.getWidth());
    273             }
    274 
    275             if (maxPreviewWidth > 0) {
    276                 maxPreviewWidth += 2 * PREVIEW_HGAP; // 2x for left and right side
    277                 maxPreviewWidth += LARGE_SHADOWS ? SHADOW_SIZE : SMALL_SHADOW_SIZE;
    278             }
    279 
    280             return maxPreviewWidth;
    281         }
    282 
    283         return 0;
    284     }
    285 
    286     /**
    287      * Layout Algorithm. This sets the {@link RenderPreview#getX()} and
    288      * {@link RenderPreview#getY()} coordinates of all the previews. It also
    289      * marks previews as visible or invisible via
    290      * {@link RenderPreview#setVisible(boolean)} according to their position and
    291      * the current visible view port in the layout canvas. Finally, it also sets
    292      * the {@code mLayoutHeight} field, such that the scrollbars can compute the
    293      * right scrolled area, and that scrolling can cause render refreshes on
    294      * views that are made visible.
    295      * <p>
    296      * This is not a traditional bin packing problem, because the objects to be
    297      * packaged do not have a fixed size; we can scale them up and down in order
    298      * to provide an "optimal" size.
    299      * <p>
    300      * See http://en.wikipedia.org/wiki/Packing_problem See
    301      * http://en.wikipedia.org/wiki/Bin_packing_problem
    302      */
    303     void layout(boolean refresh) {
    304         mNeedLayout = false;
    305 
    306         if (mPreviews == null || mPreviews.isEmpty()) {
    307             return;
    308         }
    309 
    310         int scaledImageWidth = mHScale.getScaledImgSize();
    311         int scaledImageHeight = mVScale.getScaledImgSize();
    312         Rectangle clientArea = mCanvas.getClientArea();
    313 
    314         if (!refresh &&
    315                 (scaledImageWidth == mPrevImageWidth
    316                 && scaledImageHeight == mPrevImageHeight
    317                 && clientArea.width == mPrevCanvasWidth
    318                 && clientArea.height == mPrevCanvasHeight)) {
    319             // No change
    320             return;
    321         }
    322 
    323         mPrevImageWidth = scaledImageWidth;
    324         mPrevImageHeight = scaledImageHeight;
    325         mPrevCanvasWidth = clientArea.width;
    326         mPrevCanvasHeight = clientArea.height;
    327 
    328         if (mListener == null) {
    329             mListener = new ScrollBarListener();
    330             mCanvas.getVerticalBar().addSelectionListener(mListener);
    331         }
    332 
    333         beginRenderScheduling();
    334 
    335         mLayoutHeight = 0;
    336 
    337         if (previewsHaveIdenticalSize() || fixedOrder()) {
    338             // If all the preview boxes are of identical sizes, or if the order is predetermined,
    339             // just lay them out in rows.
    340             rowLayout();
    341         } else if (previewsFit()) {
    342             layoutFullFit();
    343         } else {
    344             rowLayout();
    345         }
    346 
    347         mCanvas.updateScrollBars();
    348     }
    349 
    350     /**
    351      * Performs a simple layout where the views are laid out in a row, wrapping
    352      * around the top left canvas image.
    353      */
    354     private void rowLayout() {
    355         // TODO: Separate layout heuristics for portrait and landscape orientations (though
    356         // it also depends on the dimensions of the canvas window, which determines the
    357         // shape of the leftover space)
    358 
    359         int scaledImageWidth = mHScale.getScaledImgSize();
    360         int scaledImageHeight = mVScale.getScaledImgSize();
    361         Rectangle clientArea = mCanvas.getClientArea();
    362 
    363         int availableWidth = clientArea.x + clientArea.width - getX();
    364         int availableHeight = clientArea.y + clientArea.height - getY();
    365         int maxVisibleY = clientArea.y + clientArea.height;
    366 
    367         int bottomBorder = scaledImageHeight;
    368         int rightHandSide = scaledImageWidth + PREVIEW_HGAP;
    369         int nextY = 0;
    370 
    371         // First lay out images across the top right hand side
    372         int x = rightHandSide;
    373         int y = 0;
    374         boolean wrapped = false;
    375 
    376         int vgap = PREVIEW_VGAP;
    377         for (RenderPreview preview : mPreviews) {
    378             // If we have forked previews, double the vgap to allow space for two labels
    379             if (preview.isForked()) {
    380                 vgap *= 2;
    381                 break;
    382             }
    383         }
    384 
    385         List<RenderPreview> aspectOrder;
    386         if (!fixedOrder()) {
    387             aspectOrder = new ArrayList<RenderPreview>(mPreviews);
    388             Collections.sort(aspectOrder, RenderPreview.INCREASING_ASPECT_RATIO);
    389         } else {
    390             aspectOrder = mPreviews;
    391         }
    392 
    393         for (RenderPreview preview : aspectOrder) {
    394             if (x > 0 && x + preview.getWidth() > availableWidth) {
    395                 x = rightHandSide;
    396                 int prevY = y;
    397                 y = nextY;
    398                 if ((prevY <= bottomBorder ||
    399                         y <= bottomBorder)
    400                             && Math.max(nextY, y + preview.getHeight()) > bottomBorder) {
    401                     // If there's really no visible room below, don't bother
    402                     // Similarly, don't wrap individually scaled views
    403                     if (bottomBorder < availableHeight - 40 && preview.getScale() < 1.2) {
    404                         // If it's closer to the top row than the bottom, just
    405                         // mark the next row for left justify instead
    406                         if (bottomBorder - y > y + preview.getHeight() - bottomBorder) {
    407                             rightHandSide = 0;
    408                             wrapped = true;
    409                         } else if (!wrapped) {
    410                             y = nextY = Math.max(nextY, bottomBorder + vgap);
    411                             x = rightHandSide = 0;
    412                             wrapped = true;
    413                         }
    414                     }
    415                 }
    416             }
    417             if (x > 0 && y <= bottomBorder
    418                     && Math.max(nextY, y + preview.getHeight()) > bottomBorder) {
    419                 if (clientArea.height - bottomBorder < preview.getHeight()) {
    420                     // No room below the device on the left; just continue on the
    421                     // bottom row
    422                 } else if (preview.getScale() < 1.2) {
    423                     if (bottomBorder - y > y + preview.getHeight() - bottomBorder) {
    424                         rightHandSide = 0;
    425                         wrapped = true;
    426                     } else {
    427                         y = nextY = Math.max(nextY, bottomBorder + vgap);
    428                         x = rightHandSide = 0;
    429                         wrapped = true;
    430                     }
    431                 }
    432             }
    433 
    434             preview.setPosition(x, y);
    435 
    436             if (y > maxVisibleY && maxVisibleY > 0) {
    437                 preview.setVisible(false);
    438             } else if (!preview.isVisible()) {
    439                 preview.setVisible(true);
    440             }
    441 
    442             x += preview.getWidth();
    443             x += PREVIEW_HGAP;
    444             nextY = Math.max(nextY, y + preview.getHeight() + vgap);
    445         }
    446 
    447         mLayoutHeight = nextY;
    448     }
    449 
    450     private boolean fixedOrder() {
    451         return mMode == SCREENS;
    452     }
    453 
    454     /** Returns true if all the previews have the same identical size */
    455     private boolean previewsHaveIdenticalSize() {
    456         if (!hasPreviews()) {
    457             return true;
    458         }
    459 
    460         Iterator<RenderPreview> iterator = mPreviews.iterator();
    461         RenderPreview first = iterator.next();
    462         int width = first.getWidth();
    463         int height = first.getHeight();
    464 
    465         while (iterator.hasNext()) {
    466             RenderPreview preview = iterator.next();
    467             if (width != preview.getWidth() || height != preview.getHeight()) {
    468                 return false;
    469             }
    470         }
    471 
    472         return true;
    473     }
    474 
    475     /** Returns true if all the previews can fully fit in the available space */
    476     private boolean previewsFit() {
    477         int scaledImageWidth = mHScale.getScaledImgSize();
    478         int scaledImageHeight = mVScale.getScaledImgSize();
    479         Rectangle clientArea = mCanvas.getClientArea();
    480         int availableWidth = clientArea.x + clientArea.width - getX();
    481         int availableHeight = clientArea.y + clientArea.height - getY();
    482         int bottomBorder = scaledImageHeight;
    483         int rightHandSide = scaledImageWidth + PREVIEW_HGAP;
    484 
    485         // First see if we can fit everything; if so, we can try to make the layouts
    486         // larger such that they fill up all the available space
    487         long availableArea = rightHandSide * bottomBorder +
    488                 availableWidth * (Math.max(0, availableHeight - bottomBorder));
    489 
    490         long requiredArea = 0;
    491         for (RenderPreview preview : mPreviews) {
    492             // Note: This does not include individual preview scale; the layout
    493             // algorithm itself may be tweaking the scales to fit elements within
    494             // the layout
    495             requiredArea += preview.getArea();
    496         }
    497 
    498         return requiredArea * sScale < availableArea;
    499     }
    500 
    501     private void layoutFullFit() {
    502         int scaledImageWidth = mHScale.getScaledImgSize();
    503         int scaledImageHeight = mVScale.getScaledImgSize();
    504         Rectangle clientArea = mCanvas.getClientArea();
    505         int availableWidth = clientArea.x + clientArea.width - getX();
    506         int availableHeight = clientArea.y + clientArea.height - getY();
    507         int maxVisibleY = clientArea.y + clientArea.height;
    508         int bottomBorder = scaledImageHeight;
    509         int rightHandSide = scaledImageWidth + PREVIEW_HGAP;
    510 
    511         int minWidth = Integer.MAX_VALUE;
    512         int minHeight = Integer.MAX_VALUE;
    513         for (RenderPreview preview : mPreviews) {
    514             minWidth = Math.min(minWidth, preview.getWidth());
    515             minHeight = Math.min(minHeight, preview.getHeight());
    516         }
    517 
    518         BinPacker packer = new BinPacker(minWidth, minHeight);
    519 
    520         // TODO: Instead of this, just start with client area and occupy scaled image size!
    521 
    522         // Add in gap on right and bottom since we'll add that requirement on the width and
    523         // height rectangles too (for spacing)
    524         packer.addSpace(new Rect(rightHandSide, 0,
    525                 availableWidth - rightHandSide + PREVIEW_HGAP,
    526                 availableHeight + PREVIEW_VGAP));
    527         if (maxVisibleY > bottomBorder) {
    528             packer.addSpace(new Rect(0, bottomBorder + PREVIEW_VGAP,
    529                     availableWidth + PREVIEW_HGAP, maxVisibleY - bottomBorder + PREVIEW_VGAP));
    530         }
    531 
    532         // TODO: Sort previews first before attempting to position them?
    533 
    534         ArrayList<RenderPreview> aspectOrder = new ArrayList<RenderPreview>(mPreviews);
    535         Collections.sort(aspectOrder, RenderPreview.INCREASING_ASPECT_RATIO);
    536 
    537         for (RenderPreview preview : aspectOrder) {
    538             int previewWidth = preview.getWidth();
    539             int previewHeight = preview.getHeight();
    540             previewHeight += PREVIEW_VGAP;
    541             if (preview.isForked()) {
    542                 previewHeight += PREVIEW_VGAP;
    543             }
    544             previewWidth += PREVIEW_HGAP;
    545             // title height? how do I account for that?
    546             Rect position = packer.occupy(previewWidth, previewHeight);
    547             if (position != null) {
    548                 preview.setPosition(position.x, position.y);
    549                 preview.setVisible(true);
    550             } else {
    551                 // Can't fit: give up and do plain row layout
    552                 rowLayout();
    553                 return;
    554             }
    555         }
    556 
    557         mLayoutHeight = availableHeight;
    558     }
    559     /**
    560      * Paints the configuration previews
    561      *
    562      * @param gc the graphics context to paint into
    563      */
    564     void paint(GC gc) {
    565         if (hasPreviews()) {
    566             // Ensure up to date at all times; consider moving if it's too expensive
    567             layout(mNeedLayout);
    568             if (mNeedRender) {
    569                 renderPreviews();
    570             }
    571             if (mNeedZoom) {
    572                 boolean allowZoomIn = true /*mMode == NONE*/;
    573                 mCanvas.setFitScale(false /*onlyZoomOut*/, allowZoomIn);
    574                 mNeedZoom = false;
    575             }
    576             int rootX = getX();
    577             int rootY = getY();
    578 
    579             for (RenderPreview preview : mPreviews) {
    580                 if (preview.isVisible()) {
    581                     int x = rootX + preview.getX();
    582                     int y = rootY + preview.getY();
    583                     preview.paint(gc, x, y);
    584                 }
    585             }
    586 
    587             RenderPreview preview = mCanvas.getPreview();
    588             if (preview != null) {
    589                 String displayName = null;
    590                 Configuration configuration = preview.getConfiguration();
    591                 if (configuration instanceof VaryingConfiguration) {
    592                     // Use override flags from stashed preview, but configuration
    593                     // data from live (not varying) configured configuration
    594                     VaryingConfiguration cfg = (VaryingConfiguration) configuration;
    595                     int flags = cfg.getAlternateFlags() | cfg.getOverrideFlags();
    596                     displayName = NestedConfiguration.computeDisplayName(flags,
    597                             getChooser().getConfiguration());
    598                 } else if (configuration instanceof NestedConfiguration) {
    599                     int flags = ((NestedConfiguration) configuration).getOverrideFlags();
    600                     displayName = NestedConfiguration.computeDisplayName(flags,
    601                             getChooser().getConfiguration());
    602                 } else {
    603                     displayName = configuration.getDisplayName();
    604                 }
    605                 if (displayName != null) {
    606                     CanvasTransform hi = mHScale;
    607                     CanvasTransform vi = mVScale;
    608 
    609                     int destX = hi.translate(0);
    610                     int destY = vi.translate(0);
    611                     int destWidth = hi.getScaledImgSize();
    612                     int destHeight = vi.getScaledImgSize();
    613 
    614                     int x = destX + destWidth / 2 - preview.getWidth() / 2;
    615                     int y = destY + destHeight;
    616 
    617                     preview.paintTitle(gc, x, y, false /*showFile*/, displayName);
    618                 }
    619             }
    620 
    621             // Zoom overlay
    622             int x = getZoomX();
    623             if (x > 0) {
    624                 int y = getZoomY();
    625                 int oldAlpha = gc.getAlpha();
    626 
    627                 // Paint background oval rectangle behind the zoom and close icons
    628                 gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY));
    629                 gc.setAlpha(128);
    630                 int padding = 3;
    631                 int arc = 5;
    632                 gc.fillRoundRectangle(x - padding, y - padding,
    633                         ZOOM_ICON_WIDTH + 2 * padding,
    634                         4 * ZOOM_ICON_HEIGHT + 2 * padding, arc, arc);
    635 
    636                 gc.setAlpha(255);
    637                 IconFactory iconFactory = IconFactory.getInstance();
    638                 Image zoomOut = iconFactory.getIcon("zoomminus"); //$NON-NLS-1$);
    639                 Image zoomIn = iconFactory.getIcon("zoomplus");   //$NON-NLS-1$);
    640                 Image zoom100 = iconFactory.getIcon("zoom100");   //$NON-NLS-1$);
    641                 Image close = iconFactory.getIcon("close");       //$NON-NLS-1$);
    642 
    643                 gc.drawImage(zoomIn, x, y);
    644                 y += ZOOM_ICON_HEIGHT;
    645                 gc.drawImage(zoomOut, x, y);
    646                 y += ZOOM_ICON_HEIGHT;
    647                 gc.drawImage(zoom100, x, y);
    648                 y += ZOOM_ICON_HEIGHT;
    649                 gc.drawImage(close, x, y);
    650                 y += ZOOM_ICON_HEIGHT;
    651                 gc.setAlpha(oldAlpha);
    652             }
    653         } else if (mMode == CUSTOM) {
    654             int rootX = getX();
    655             rootX += mHScale.getScaledImgSize();
    656             rootX += 2 * PREVIEW_HGAP;
    657             int rootY = getY();
    658             rootY += 20;
    659             gc.setFont(mCanvas.getFont());
    660             gc.setForeground(mCanvas.getDisplay().getSystemColor(SWT.COLOR_BLACK));
    661             gc.drawText("Add previews with \"Add as Thumbnail\"\nin the configuration menu",
    662                     rootX, rootY, true);
    663         }
    664 
    665         if (mAnimation != null) {
    666             mAnimation.tick(gc);
    667         }
    668     }
    669 
    670     private void addPreview(@NonNull RenderPreview preview) {
    671         if (mPreviews == null) {
    672             mPreviews = Lists.newArrayList();
    673         }
    674         mPreviews.add(preview);
    675     }
    676 
    677     /** Adds the current configuration as a new configuration preview */
    678     public void addAsThumbnail() {
    679         ConfigurationChooser chooser = getChooser();
    680         String name = chooser.getConfiguration().getDisplayName();
    681         if (name == null || name.isEmpty()) {
    682             name = getUniqueName();
    683         }
    684         InputDialog d = new InputDialog(
    685                 AdtPlugin.getShell(),
    686                 "Add as Thumbnail Preview",  // title
    687                 "Name of thumbnail:",
    688                 name,
    689                 null);
    690         if (d.open() == Window.OK) {
    691             selectMode(CUSTOM);
    692 
    693             String newName = d.getValue();
    694             // Create a new configuration from the current settings in the composite
    695             Configuration configuration = Configuration.copy(chooser.getConfiguration());
    696             configuration.setDisplayName(newName);
    697 
    698             RenderPreview preview = RenderPreview.create(this, configuration);
    699             addPreview(preview);
    700 
    701             layout(true);
    702             beginRenderScheduling();
    703             scheduleRender(preview);
    704             mCanvas.setFitScale(true /* onlyZoomOut */, false /*allowZoomIn*/);
    705 
    706             if (mManualList == null) {
    707                 loadList();
    708             }
    709             if (mManualList != null) {
    710                 mManualList.add(preview);
    711                 saveList();
    712             }
    713         }
    714     }
    715 
    716     /**
    717      * Computes a unique new name for a configuration preview that represents
    718      * the current, default configuration
    719      *
    720      * @return a unique name
    721      */
    722     private String getUniqueName() {
    723         if (mPreviews == null || mPreviews.isEmpty()) {
    724             // NO, not for the first preview!
    725             return "Config1";
    726         }
    727 
    728         Set<String> names = new HashSet<String>(mPreviews.size());
    729         for (RenderPreview preview : mPreviews) {
    730             names.add(preview.getDisplayName());
    731         }
    732 
    733         int index = 2;
    734         while (true) {
    735             String name = String.format("Config%1$d", index);
    736             if (!names.contains(name)) {
    737                 return name;
    738             }
    739             index++;
    740         }
    741     }
    742 
    743     /** Generates a bunch of default configuration preview thumbnails */
    744     public void addDefaultPreviews() {
    745         ConfigurationChooser chooser = getChooser();
    746         Configuration parent = chooser.getConfiguration();
    747         if (parent instanceof NestedConfiguration) {
    748             parent = ((NestedConfiguration) parent).getParent();
    749         }
    750         if (mCanvas.getImageOverlay().getImage() != null) {
    751             // Create Language variation
    752             createLocaleVariation(chooser, parent);
    753 
    754             // Vary screen size
    755             // TODO: Be smarter here: Pick a screen that is both as differently as possible
    756             // from the current screen as well as also supported. So consider
    757             // things like supported screens, targetSdk etc.
    758             createScreenVariations(parent);
    759 
    760             // Vary orientation
    761             createStateVariation(chooser, parent);
    762 
    763             // Vary render target
    764             createRenderTargetVariation(chooser, parent);
    765         }
    766 
    767         // Also add in include-context previews, if any
    768         addIncludedInPreviews();
    769 
    770         // Make a placeholder preview for the current screen, in case we switch from it
    771         RenderPreview preview = RenderPreview.create(this, parent);
    772         mCanvas.setPreview(preview);
    773 
    774         sortPreviewsByOrientation();
    775     }
    776 
    777     private void createRenderTargetVariation(ConfigurationChooser chooser, Configuration parent) {
    778         /* This is disabled for now: need to load multiple versions of layoutlib.
    779         When I did this, there seemed to be some drug interactions between
    780         them, and I would end up with NPEs in layoutlib code which normally works.
    781         VaryingConfiguration configuration =
    782                 VaryingConfiguration.create(chooser, parent);
    783         configuration.setAlternatingTarget(true);
    784         configuration.syncFolderConfig();
    785         addPreview(RenderPreview.create(this, configuration));
    786         */
    787     }
    788 
    789     private void createStateVariation(ConfigurationChooser chooser, Configuration parent) {
    790         State currentState = parent.getDeviceState();
    791         State nextState = parent.getNextDeviceState(currentState);
    792         if (nextState != currentState) {
    793             VaryingConfiguration configuration =
    794                     VaryingConfiguration.create(chooser, parent);
    795             configuration.setAlternateDeviceState(true);
    796             configuration.syncFolderConfig();
    797             addPreview(RenderPreview.create(this, configuration));
    798         }
    799     }
    800 
    801     private void createLocaleVariation(ConfigurationChooser chooser, Configuration parent) {
    802         LanguageQualifier currentLanguage = parent.getLocale().language;
    803         for (Locale locale : chooser.getLocaleList()) {
    804             LanguageQualifier language = locale.language;
    805             if (!language.equals(currentLanguage)) {
    806                 VaryingConfiguration configuration =
    807                         VaryingConfiguration.create(chooser, parent);
    808                 configuration.setAlternateLocale(true);
    809                 configuration.syncFolderConfig();
    810                 addPreview(RenderPreview.create(this, configuration));
    811                 break;
    812             }
    813         }
    814     }
    815 
    816     private void createScreenVariations(Configuration parent) {
    817         ConfigurationChooser chooser = getChooser();
    818         VaryingConfiguration configuration;
    819 
    820         configuration = VaryingConfiguration.create(chooser, parent);
    821         configuration.setVariation(0);
    822         configuration.setAlternateDevice(true);
    823         configuration.syncFolderConfig();
    824         addPreview(RenderPreview.create(this, configuration));
    825 
    826         configuration = VaryingConfiguration.create(chooser, parent);
    827         configuration.setVariation(1);
    828         configuration.setAlternateDevice(true);
    829         configuration.syncFolderConfig();
    830         addPreview(RenderPreview.create(this, configuration));
    831     }
    832 
    833     /**
    834      * Returns the current mode as seen by this {@link RenderPreviewManager}.
    835      * Note that it may not yet have been synced with the global mode kept in
    836      * {@link AdtPrefs#getRenderPreviewMode()}.
    837      *
    838      * @return the current preview mode
    839      */
    840     @NonNull
    841     public RenderPreviewMode getMode() {
    842         return mMode;
    843     }
    844 
    845     /**
    846      * Update the set of previews for the current mode
    847      *
    848      * @param force force a refresh even if the preview type has not changed
    849      * @return true if the views were recomputed, false if the previews were
    850      *         already showing and the mode not changed
    851      */
    852     public boolean recomputePreviews(boolean force) {
    853         RenderPreviewMode newMode = AdtPrefs.getPrefs().getRenderPreviewMode();
    854         if (newMode == mMode && !force
    855                 && (mRevision == sRevision
    856                     || mMode == NONE
    857                     || mMode == CUSTOM)) {
    858             return false;
    859         }
    860 
    861         RenderPreviewMode oldMode = mMode;
    862         mMode = newMode;
    863         mRevision = sRevision;
    864 
    865         sScale = 1.0;
    866         disposePreviews();
    867 
    868         switch (mMode) {
    869             case DEFAULT:
    870                 addDefaultPreviews();
    871                 break;
    872             case INCLUDES:
    873                 addIncludedInPreviews();
    874                 break;
    875             case LOCALES:
    876                 addLocalePreviews();
    877                 break;
    878             case SCREENS:
    879                 addScreenSizePreviews();
    880                 break;
    881             case VARIATIONS:
    882                 addVariationPreviews();
    883                 break;
    884             case CUSTOM:
    885                 addManualPreviews();
    886                 break;
    887             case NONE:
    888                 // Can't just set mNeedZoom because with no previews, the paint
    889                 // method does nothing
    890                 mCanvas.setFitScale(false /*onlyZoomOut*/, true /*allowZoomIn*/);
    891                 break;
    892             default:
    893                 assert false : mMode;
    894         }
    895 
    896         // We schedule layout for the next redraw rather than process it here immediately;
    897         // not only does this let us avoid doing work for windows where the tab is in the
    898         // background, but when a file is opened we may not know the size of the canvas
    899         // yet, and the layout methods need it in order to do a good job. By the time
    900         // the canvas is painted, we have accurate bounds.
    901         mNeedLayout = mNeedRender = true;
    902         mCanvas.redraw();
    903 
    904         if (oldMode != mMode && (oldMode == NONE || mMode == NONE)) {
    905             // If entering or exiting preview mode: updating padding which is compressed
    906             // only in preview mode.
    907             mCanvas.getHorizontalTransform().refresh();
    908             mCanvas.getVerticalTransform().refresh();
    909         }
    910 
    911         return true;
    912     }
    913 
    914     /**
    915      * Sets the new render preview mode to use
    916      *
    917      * @param mode the new mode
    918      */
    919     public void selectMode(@NonNull RenderPreviewMode mode) {
    920         if (mode != mMode) {
    921             AdtPrefs.getPrefs().setPreviewMode(mode);
    922             recomputePreviews(false);
    923         }
    924     }
    925 
    926     /** Similar to {@link #addDefaultPreviews()} but for locales */
    927     public void addLocalePreviews() {
    928 
    929         ConfigurationChooser chooser = getChooser();
    930         List<Locale> locales = chooser.getLocaleList();
    931         Configuration parent = chooser.getConfiguration();
    932 
    933         for (Locale locale : locales) {
    934             if (!locale.hasLanguage() && !locale.hasRegion()) {
    935                 continue;
    936             }
    937             NestedConfiguration configuration = NestedConfiguration.create(chooser, parent);
    938             configuration.setOverrideLocale(true);
    939             configuration.setLocale(locale, false);
    940 
    941             String displayName = ConfigurationChooser.getLocaleLabel(chooser, locale, false);
    942             assert displayName != null; // it's never non null when locale is non null
    943             configuration.setDisplayName(displayName);
    944 
    945             addPreview(RenderPreview.create(this, configuration));
    946         }
    947 
    948         // Make a placeholder preview for the current screen, in case we switch from it
    949         Configuration configuration = parent;
    950         Locale locale = configuration.getLocale();
    951         String label = ConfigurationChooser.getLocaleLabel(chooser, locale, false);
    952         if (label == null) {
    953             label = "default";
    954         }
    955         configuration.setDisplayName(label);
    956         RenderPreview preview = RenderPreview.create(this, parent);
    957         if (preview != null) {
    958             mCanvas.setPreview(preview);
    959         }
    960 
    961         // No need to sort: they should all be identical
    962     }
    963 
    964     /** Similar to {@link #addDefaultPreviews()} but for screen sizes */
    965     public void addScreenSizePreviews() {
    966         ConfigurationChooser chooser = getChooser();
    967         List<Device> devices = chooser.getDeviceList();
    968         Configuration configuration = chooser.getConfiguration();
    969         boolean canScaleNinePatch = configuration.supports(Capability.FIXED_SCALABLE_NINE_PATCH);
    970 
    971         // Rearrange the devices a bit such that the most interesting devices bubble
    972         // to the front
    973         // 10" tablet, 7" tablet, reference phones, tiny phone, and in general the first
    974         // version of each seen screen size
    975         List<Device> sorted = new ArrayList<Device>(devices);
    976         Set<ScreenSize> seenSizes = new HashSet<ScreenSize>();
    977         State currentState = configuration.getDeviceState();
    978         String currentStateName = currentState != null ? currentState.getName() : "";
    979 
    980         for (int i = 0, n = sorted.size(); i < n; i++) {
    981             Device device = sorted.get(i);
    982             boolean interesting = false;
    983 
    984             State state = device.getState(currentStateName);
    985             if (state == null) {
    986                 state = device.getAllStates().get(0);
    987             }
    988 
    989             if (device.getName().startsWith("Nexus ")         //$NON-NLS-1$
    990                     || device.getName().endsWith(" Nexus")) { //$NON-NLS-1$
    991                 // Not String#contains("Nexus") because that would also pick up all the generic
    992                 // entries ("3.7in WVGA (Nexus One)") so we'd have them duplicated
    993                 interesting = true;
    994             }
    995 
    996             FolderConfiguration c = DeviceConfigHelper.getFolderConfig(state);
    997             if (c != null) {
    998                 ScreenSizeQualifier sizeQualifier = c.getScreenSizeQualifier();
    999                 if (sizeQualifier != null) {
   1000                     ScreenSize size = sizeQualifier.getValue();
   1001                     if (!seenSizes.contains(size)) {
   1002                         seenSizes.add(size);
   1003                         interesting = true;
   1004                     }
   1005                 }
   1006 
   1007                 // Omit LDPI, not really used anymore
   1008                 DensityQualifier density = c.getDensityQualifier();
   1009                 if (density != null) {
   1010                     Density d = density.getValue();
   1011                     if (d == Density.LOW) {
   1012                         interesting = false;
   1013                     }
   1014 
   1015                     if (!canScaleNinePatch && d == Density.TV) {
   1016                         interesting = false;
   1017                     }
   1018                 }
   1019             }
   1020 
   1021             if (interesting) {
   1022                 NestedConfiguration screenConfig = NestedConfiguration.create(chooser,
   1023                         configuration);
   1024                 screenConfig.setOverrideDevice(true);
   1025                 screenConfig.setDevice(device, true);
   1026                 screenConfig.syncFolderConfig();
   1027                 screenConfig.setDisplayName(ConfigurationChooser.getDeviceLabel(device, true));
   1028                 addPreview(RenderPreview.create(this, screenConfig));
   1029             }
   1030         }
   1031 
   1032         // Sorted by screen size, in decreasing order
   1033         sortPreviewsByScreenSize();
   1034     }
   1035 
   1036     /**
   1037      * Previews this layout as included in other layouts
   1038      */
   1039     public void addIncludedInPreviews() {
   1040         ConfigurationChooser chooser = getChooser();
   1041         IProject project = chooser.getProject();
   1042         if (project == null) {
   1043             return;
   1044         }
   1045         IncludeFinder finder = IncludeFinder.get(project);
   1046 
   1047         final List<Reference> includedBy = finder.getIncludedBy(chooser.getEditedFile());
   1048 
   1049         if (includedBy == null || includedBy.isEmpty()) {
   1050             // TODO: Generate some useful defaults, such as including it in a ListView
   1051             // as the list item layout?
   1052             return;
   1053         }
   1054 
   1055         for (final Reference reference : includedBy) {
   1056             String title = reference.getDisplayName();
   1057             Configuration config = Configuration.create(chooser.getConfiguration(),
   1058                     reference.getFile());
   1059             RenderPreview preview = RenderPreview.create(this, config);
   1060             preview.setDisplayName(title);
   1061             preview.setIncludedWithin(reference);
   1062 
   1063             addPreview(preview);
   1064         }
   1065 
   1066         sortPreviewsByOrientation();
   1067     }
   1068 
   1069     /**
   1070      * Previews this layout as included in other layouts
   1071      */
   1072     public void addVariationPreviews() {
   1073         ConfigurationChooser chooser = getChooser();
   1074 
   1075         IFile file = chooser.getEditedFile();
   1076         List<IFile> variations = AdtUtils.getResourceVariations(file, false /*includeSelf*/);
   1077 
   1078         // Sort by parent folder
   1079         Collections.sort(variations, new Comparator<IFile>() {
   1080             @Override
   1081             public int compare(IFile file1, IFile file2) {
   1082                 return file1.getParent().getName().compareTo(file2.getParent().getName());
   1083             }
   1084         });
   1085 
   1086         Configuration currentConfig = chooser.getConfiguration();
   1087 
   1088         for (IFile variation : variations) {
   1089             String title = variation.getParent().getName();
   1090             Configuration config = Configuration.create(chooser.getConfiguration(), variation);
   1091             config.setTheme(currentConfig.getTheme());
   1092             config.setActivity(currentConfig.getActivity());
   1093             RenderPreview preview = RenderPreview.create(this, config);
   1094             preview.setDisplayName(title);
   1095             preview.setAlternateInput(variation);
   1096 
   1097             addPreview(preview);
   1098         }
   1099 
   1100         sortPreviewsByOrientation();
   1101     }
   1102 
   1103     /**
   1104      * Previews this layout using a custom configured set of layouts
   1105      */
   1106     public void addManualPreviews() {
   1107         if (mManualList == null) {
   1108             loadList();
   1109         } else {
   1110             mPreviews = mManualList.createPreviews(mCanvas);
   1111         }
   1112     }
   1113 
   1114     private void loadList() {
   1115         IProject project = getChooser().getProject();
   1116         if (project == null) {
   1117             return;
   1118         }
   1119 
   1120         if (mManualList == null) {
   1121             mManualList = RenderPreviewList.get(project);
   1122         }
   1123 
   1124         try {
   1125             mManualList.load(getChooser().getDeviceList());
   1126             mPreviews = mManualList.createPreviews(mCanvas);
   1127         } catch (IOException e) {
   1128             AdtPlugin.log(e, null);
   1129         }
   1130     }
   1131 
   1132     private void saveList() {
   1133         if (mManualList != null) {
   1134             try {
   1135                 mManualList.save();
   1136             } catch (IOException e) {
   1137                 AdtPlugin.log(e, null);
   1138             }
   1139         }
   1140     }
   1141 
   1142     void rename(ConfigurationDescription description, String newName) {
   1143         IProject project = getChooser().getProject();
   1144         if (project == null) {
   1145             return;
   1146         }
   1147 
   1148         if (mManualList == null) {
   1149             mManualList = RenderPreviewList.get(project);
   1150         }
   1151         description.displayName = newName;
   1152         saveList();
   1153     }
   1154 
   1155 
   1156     /**
   1157      * Notifies that the main configuration has changed.
   1158      *
   1159      * @param flags the change flags, a bitmask corresponding to the
   1160      *            {@code CHANGE_} constants in {@link ConfigurationClient}
   1161      */
   1162     public void configurationChanged(int flags) {
   1163         // Similar to renderPreviews, but only acts on incomplete previews
   1164         if (hasPreviews()) {
   1165             // Do zoomed images first
   1166             beginRenderScheduling();
   1167             for (RenderPreview preview : mPreviews) {
   1168                 if (preview.getScale() > 1.2) {
   1169                     preview.configurationChanged(flags);
   1170                 }
   1171             }
   1172             for (RenderPreview preview : mPreviews) {
   1173                 if (preview.getScale() <= 1.2) {
   1174                     preview.configurationChanged(flags);
   1175                 }
   1176             }
   1177             RenderPreview preview = mCanvas.getPreview();
   1178             if (preview != null) {
   1179                 preview.configurationChanged(flags);
   1180                 preview.dispose();
   1181             }
   1182             mNeedLayout = true;
   1183             mCanvas.redraw();
   1184         }
   1185     }
   1186 
   1187     /** Updates the configuration preview thumbnails */
   1188     public void renderPreviews() {
   1189         if (hasPreviews()) {
   1190             beginRenderScheduling();
   1191 
   1192             // Process in visual order
   1193             ArrayList<RenderPreview> visualOrder = new ArrayList<RenderPreview>(mPreviews);
   1194             Collections.sort(visualOrder, RenderPreview.VISUAL_ORDER);
   1195 
   1196             // Do zoomed images first
   1197             for (RenderPreview preview : visualOrder) {
   1198                 if (preview.getScale() > 1.2 && preview.isVisible()) {
   1199                     scheduleRender(preview);
   1200                 }
   1201             }
   1202             // Non-zoomed images
   1203             for (RenderPreview preview : visualOrder) {
   1204                 if (preview.getScale() <= 1.2 && preview.isVisible()) {
   1205                     scheduleRender(preview);
   1206                 }
   1207             }
   1208         }
   1209 
   1210         mNeedRender = false;
   1211     }
   1212 
   1213     private int mPendingRenderCount;
   1214 
   1215     /**
   1216      * Reset rendering scheduling. The next render request will be scheduled
   1217      * after a single delay unit.
   1218      */
   1219     public void beginRenderScheduling() {
   1220         mPendingRenderCount = 0;
   1221     }
   1222 
   1223     /**
   1224      * Schedule rendering the given preview. Each successive call will add an additional
   1225      * delay unit to the schedule from the previous {@link #scheduleRender(RenderPreview)}
   1226      * call, until {@link #beginRenderScheduling()} is called again.
   1227      *
   1228      * @param preview the preview to render
   1229      */
   1230     public void scheduleRender(@NonNull RenderPreview preview) {
   1231         mPendingRenderCount++;
   1232         preview.render(mPendingRenderCount * RENDER_DELAY);
   1233     }
   1234 
   1235     /**
   1236      * Switch to the given configuration preview
   1237      *
   1238      * @param preview the preview to switch to
   1239      */
   1240     public void switchTo(@NonNull RenderPreview preview) {
   1241         IFile input = preview.getAlternateInput();
   1242         if (input != null) {
   1243             IWorkbenchPartSite site = mCanvas.getEditorDelegate().getEditor().getSite();
   1244             try {
   1245                 // This switches to the given file, but the file might not have
   1246                 // an identical configuration to what was shown in the preview.
   1247                 // For example, while viewing a 10" layout-xlarge file, it might
   1248                 // show a preview for a 5" version tied to the default layout. If
   1249                 // you click on it, it will open the default layout file, but it might
   1250                 // be using a different screen size; any of those that match the
   1251                 // default layout, say a 3.8".
   1252                 //
   1253                 // Thus, we need to also perform a screen size sync first
   1254                 Configuration configuration = preview.getConfiguration();
   1255                 boolean setSize = false;
   1256                 if (configuration instanceof NestedConfiguration) {
   1257                     NestedConfiguration nestedConfig = (NestedConfiguration) configuration;
   1258                     setSize = nestedConfig.isOverridingDevice();
   1259                     if (configuration instanceof VaryingConfiguration) {
   1260                         VaryingConfiguration c = (VaryingConfiguration) configuration;
   1261                         setSize |= c.isAlternatingDevice();
   1262                     }
   1263 
   1264                     if (setSize) {
   1265                         ConfigurationChooser chooser = getChooser();
   1266                         IFile editedFile = chooser.getEditedFile();
   1267                         if (editedFile != null) {
   1268                             chooser.syncToVariations(CFG_DEVICE|CFG_DEVICE_STATE,
   1269                                     editedFile, configuration, false, false);
   1270                         }
   1271                     }
   1272                 }
   1273 
   1274                 IDE.openEditor(site.getWorkbenchWindow().getActivePage(), input,
   1275                         CommonXmlEditor.ID);
   1276             } catch (PartInitException e) {
   1277                 AdtPlugin.log(e, null);
   1278             }
   1279             return;
   1280         }
   1281 
   1282         GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor();
   1283         ConfigurationChooser chooser = editor.getConfigurationChooser();
   1284 
   1285         Configuration originalConfiguration = chooser.getConfiguration();
   1286 
   1287         // The new configuration is the configuration which will become the configuration
   1288         // in the layout editor's chooser
   1289         Configuration previewConfiguration = preview.getConfiguration();
   1290         Configuration newConfiguration = previewConfiguration;
   1291         if (newConfiguration instanceof NestedConfiguration) {
   1292             // Should never use a complementing configuration for the main
   1293             // rendering's configuration; instead, create a new configuration
   1294             // with a snapshot of the configuration's current values
   1295             newConfiguration = Configuration.copy(previewConfiguration);
   1296 
   1297             // Remap all the previews to be parented to this new copy instead
   1298             // of the old one (which is no longer controlled by the chooser)
   1299             for (RenderPreview p : mPreviews) {
   1300                 Configuration configuration = p.getConfiguration();
   1301                 if (configuration instanceof NestedConfiguration) {
   1302                     NestedConfiguration nested = (NestedConfiguration) configuration;
   1303                     nested.setParent(newConfiguration);
   1304                 }
   1305             }
   1306         }
   1307 
   1308         // Make a preview for the configuration which *was* showing in the
   1309         // chooser up until this point:
   1310         RenderPreview newPreview = mCanvas.getPreview();
   1311         if (newPreview == null) {
   1312             newPreview = RenderPreview.create(this, originalConfiguration);
   1313         }
   1314 
   1315         // Update its configuration such that it is complementing or inheriting
   1316         // from the new chosen configuration
   1317         if (previewConfiguration instanceof VaryingConfiguration) {
   1318             VaryingConfiguration varying = VaryingConfiguration.create(
   1319                     (VaryingConfiguration) previewConfiguration,
   1320                     newConfiguration);
   1321             varying.updateDisplayName();
   1322             originalConfiguration = varying;
   1323             newPreview.setConfiguration(originalConfiguration);
   1324         } else if (previewConfiguration instanceof NestedConfiguration) {
   1325             NestedConfiguration nested = NestedConfiguration.create(
   1326                     (NestedConfiguration) previewConfiguration,
   1327                     originalConfiguration,
   1328                     newConfiguration);
   1329             nested.setDisplayName(nested.computeDisplayName());
   1330             originalConfiguration = nested;
   1331             newPreview.setConfiguration(originalConfiguration);
   1332         }
   1333 
   1334         // Replace clicked preview with preview of the formerly edited main configuration
   1335         // This doesn't work yet because the image overlay has had its image
   1336         // replaced by the configuration previews! I should make a list of them
   1337         //newPreview.setFullImage(mImageOverlay.getAwtImage());
   1338         for (int i = 0, n = mPreviews.size(); i < n; i++) {
   1339             if (preview == mPreviews.get(i)) {
   1340                 mPreviews.set(i, newPreview);
   1341                 break;
   1342             }
   1343         }
   1344 
   1345         // Stash the corresponding preview (not active) on the canvas so we can
   1346         // retrieve it if clicking to some other preview later
   1347         mCanvas.setPreview(preview);
   1348         preview.setVisible(false);
   1349 
   1350         // Switch to the configuration from the clicked preview (though it's
   1351         // most likely a copy, see above)
   1352         chooser.setConfiguration(newConfiguration);
   1353         editor.changed(MASK_ALL);
   1354 
   1355         // Scroll to the top again, if necessary
   1356         mCanvas.getVerticalBar().setSelection(mCanvas.getVerticalBar().getMinimum());
   1357 
   1358         mNeedLayout = mNeedZoom = true;
   1359         mCanvas.redraw();
   1360         mAnimation = new SwapAnimation(preview, newPreview);
   1361     }
   1362 
   1363     /**
   1364      * Gets the preview at the given location, or null if none. This is
   1365      * currently deeply tied to where things are painted in onPaint().
   1366      */
   1367     RenderPreview getPreview(ControlPoint mousePos) {
   1368         if (hasPreviews()) {
   1369             int rootX = getX();
   1370             if (mousePos.x < rootX) {
   1371                 return null;
   1372             }
   1373             int rootY = getY();
   1374 
   1375             for (RenderPreview preview : mPreviews) {
   1376                 int x = rootX + preview.getX();
   1377                 int y = rootY + preview.getY();
   1378                 if (mousePos.x >= x && mousePos.x <= x + preview.getWidth()) {
   1379                     if (mousePos.y >= y && mousePos.y <= y + preview.getHeight()) {
   1380                         return preview;
   1381                     }
   1382                 }
   1383             }
   1384         }
   1385 
   1386         return null;
   1387     }
   1388 
   1389     private int getX() {
   1390         return mHScale.translate(0);
   1391     }
   1392 
   1393     private int getY() {
   1394         return mVScale.translate(0);
   1395     }
   1396 
   1397     private int getZoomX() {
   1398         Rectangle clientArea = mCanvas.getClientArea();
   1399         int x = clientArea.x + clientArea.width - ZOOM_ICON_WIDTH;
   1400         if (x < mHScale.getScaledImgSize() + PREVIEW_HGAP) {
   1401             // No visible previews because the main image is zoomed too far
   1402             return -1;
   1403         }
   1404 
   1405         return x - 6;
   1406     }
   1407 
   1408     private int getZoomY() {
   1409         Rectangle clientArea = mCanvas.getClientArea();
   1410         return clientArea.y + 5;
   1411     }
   1412 
   1413     /**
   1414      * Returns the height of the layout
   1415      *
   1416      * @return the height
   1417      */
   1418     public int getHeight() {
   1419         return mLayoutHeight;
   1420     }
   1421 
   1422     /**
   1423      * Notifies that preview manager that the mouse cursor has moved to the
   1424      * given control position within the layout canvas
   1425      *
   1426      * @param mousePos the mouse position, relative to the layout canvas
   1427      */
   1428     public void moved(ControlPoint mousePos) {
   1429         RenderPreview hovered = getPreview(mousePos);
   1430         if (hovered != mActivePreview) {
   1431             if (mActivePreview != null) {
   1432                 mActivePreview.setActive(false);
   1433             }
   1434             mActivePreview = hovered;
   1435             if (mActivePreview != null) {
   1436                 mActivePreview.setActive(true);
   1437             }
   1438             mCanvas.redraw();
   1439         }
   1440     }
   1441 
   1442     /**
   1443      * Notifies that preview manager that the mouse cursor has entered the layout canvas
   1444      *
   1445      * @param mousePos the mouse position, relative to the layout canvas
   1446      */
   1447     public void enter(ControlPoint mousePos) {
   1448         moved(mousePos);
   1449     }
   1450 
   1451     /**
   1452      * Notifies that preview manager that the mouse cursor has exited the layout canvas
   1453      *
   1454      * @param mousePos the mouse position, relative to the layout canvas
   1455      */
   1456     public void exit(ControlPoint mousePos) {
   1457         if (mActivePreview != null) {
   1458             mActivePreview.setActive(false);
   1459         }
   1460         mActivePreview = null;
   1461         mCanvas.redraw();
   1462     }
   1463 
   1464     /**
   1465      * Process a mouse click, and return true if it was handled by this manager
   1466      * (e.g. the click was on a preview)
   1467      *
   1468      * @param mousePos the mouse position where the click occurred
   1469      * @return true if the click occurred over a preview and was handled, false otherwise
   1470      */
   1471     public boolean click(ControlPoint mousePos) {
   1472         // Clicked zoom?
   1473         int x = getZoomX();
   1474         if (x > 0) {
   1475             if (mousePos.x >= x && mousePos.x <= x + ZOOM_ICON_WIDTH) {
   1476                 int y = getZoomY();
   1477                 if (mousePos.y >= y && mousePos.y <= y + 4 * ZOOM_ICON_HEIGHT) {
   1478                     if (mousePos.y < y + ZOOM_ICON_HEIGHT) {
   1479                         zoomIn();
   1480                     } else if (mousePos.y < y + 2 * ZOOM_ICON_HEIGHT) {
   1481                         zoomOut();
   1482                     } else if (mousePos.y < y + 3 * ZOOM_ICON_HEIGHT) {
   1483                         zoomReset();
   1484                     } else {
   1485                         selectMode(NONE);
   1486                     }
   1487                     return true;
   1488                 }
   1489             }
   1490         }
   1491 
   1492         RenderPreview preview = getPreview(mousePos);
   1493         if (preview != null) {
   1494             boolean handled = preview.click(mousePos.x - getX() - preview.getX(),
   1495                     mousePos.y - getY() - preview.getY());
   1496             if (handled) {
   1497                 // In case layout was performed, there could be a new preview
   1498                 // under this coordinate now, so make sure it's hover etc
   1499                 // shows up
   1500                 moved(mousePos);
   1501                 return true;
   1502             }
   1503         }
   1504 
   1505         return false;
   1506     }
   1507 
   1508     /**
   1509      * Returns true if there are thumbnail previews
   1510      *
   1511      * @return true if thumbnails are being shown
   1512      */
   1513     public boolean hasPreviews() {
   1514         return mPreviews != null && !mPreviews.isEmpty();
   1515     }
   1516 
   1517 
   1518     private void sortPreviewsByScreenSize() {
   1519         if (mPreviews != null) {
   1520             Collections.sort(mPreviews, new Comparator<RenderPreview>() {
   1521                 @Override
   1522                 public int compare(RenderPreview preview1, RenderPreview preview2) {
   1523                     Configuration config1 = preview1.getConfiguration();
   1524                     Configuration config2 = preview2.getConfiguration();
   1525                     Device device1 = config1.getDevice();
   1526                     Device device2 = config1.getDevice();
   1527                     if (device1 != null && device2 != null) {
   1528                         Screen screen1 = device1.getDefaultHardware().getScreen();
   1529                         Screen screen2 = device2.getDefaultHardware().getScreen();
   1530                         if (screen1 != null && screen2 != null) {
   1531                             double delta = screen1.getDiagonalLength()
   1532                                     - screen2.getDiagonalLength();
   1533                             if (delta != 0.0) {
   1534                                 return (int) Math.signum(delta);
   1535                             } else {
   1536                                 if (screen1.getPixelDensity() != screen2.getPixelDensity()) {
   1537                                     return screen1.getPixelDensity().compareTo(
   1538                                             screen2.getPixelDensity());
   1539                                 }
   1540                             }
   1541                         }
   1542 
   1543                     }
   1544                     State state1 = config1.getDeviceState();
   1545                     State state2 = config2.getDeviceState();
   1546                     if (state1 != state2 && state1 != null && state2 != null) {
   1547                         return state1.getName().compareTo(state2.getName());
   1548                     }
   1549 
   1550                     return preview1.getDisplayName().compareTo(preview2.getDisplayName());
   1551                 }
   1552             });
   1553         }
   1554     }
   1555 
   1556     private void sortPreviewsByOrientation() {
   1557         if (mPreviews != null) {
   1558             Collections.sort(mPreviews, new Comparator<RenderPreview>() {
   1559                 @Override
   1560                 public int compare(RenderPreview preview1, RenderPreview preview2) {
   1561                     Configuration config1 = preview1.getConfiguration();
   1562                     Configuration config2 = preview2.getConfiguration();
   1563                     State state1 = config1.getDeviceState();
   1564                     State state2 = config2.getDeviceState();
   1565                     if (state1 != state2 && state1 != null && state2 != null) {
   1566                         return state1.getName().compareTo(state2.getName());
   1567                     }
   1568 
   1569                     return preview1.getDisplayName().compareTo(preview2.getDisplayName());
   1570                 }
   1571             });
   1572         }
   1573     }
   1574 
   1575     /**
   1576      * Vertical scrollbar listener which updates render previews which are not
   1577      * visible and triggers a redraw
   1578      */
   1579     private class ScrollBarListener implements SelectionListener {
   1580         @Override
   1581         public void widgetSelected(SelectionEvent e) {
   1582             if (mPreviews == null) {
   1583                 return;
   1584             }
   1585 
   1586             ScrollBar bar = mCanvas.getVerticalBar();
   1587             int selection = bar.getSelection();
   1588             int thumb = bar.getThumb();
   1589             int maxY = selection + thumb;
   1590             beginRenderScheduling();
   1591             for (RenderPreview preview : mPreviews) {
   1592                 if (!preview.isVisible() && preview.getY() <= maxY) {
   1593                     preview.setVisible(true);
   1594                 }
   1595             }
   1596         }
   1597 
   1598         @Override
   1599         public void widgetDefaultSelected(SelectionEvent e) {
   1600         }
   1601     }
   1602 
   1603     /** Animation overlay shown briefly after swapping two previews */
   1604     private class SwapAnimation implements Runnable {
   1605         private long begin;
   1606         private long end;
   1607         private static final long DURATION = 400; // ms
   1608         private Rect initialRect1;
   1609         private Rect targetRect1;
   1610         private Rect initialRect2;
   1611         private Rect targetRect2;
   1612         private RenderPreview preview;
   1613 
   1614         SwapAnimation(RenderPreview preview1, RenderPreview preview2) {
   1615             begin = System.currentTimeMillis();
   1616             end = begin + DURATION;
   1617 
   1618             initialRect1 = new Rect(preview1.getX(), preview1.getY(),
   1619                     preview1.getWidth(), preview1.getHeight());
   1620 
   1621             CanvasTransform hi = mCanvas.getHorizontalTransform();
   1622             CanvasTransform vi = mCanvas.getVerticalTransform();
   1623             initialRect2 = new Rect(hi.translate(0), vi.translate(0),
   1624                     hi.getScaledImgSize(), vi.getScaledImgSize());
   1625             preview = preview2;
   1626         }
   1627 
   1628         void tick(GC gc) {
   1629             long now = System.currentTimeMillis();
   1630             if (now > end || mCanvas.isDisposed()) {
   1631                 mAnimation = null;
   1632                 return;
   1633             }
   1634 
   1635             CanvasTransform hi = mCanvas.getHorizontalTransform();
   1636             CanvasTransform vi = mCanvas.getVerticalTransform();
   1637             if (targetRect1 == null) {
   1638                 targetRect1 = new Rect(hi.translate(0), vi.translate(0),
   1639                     hi.getScaledImgSize(), vi.getScaledImgSize());
   1640             }
   1641             double portion = (now - begin) / (double) DURATION;
   1642             Rect rect1 = new Rect(
   1643                     (int) (portion * (targetRect1.x - initialRect1.x) + initialRect1.x),
   1644                     (int) (portion * (targetRect1.y - initialRect1.y) + initialRect1.y),
   1645                     (int) (portion * (targetRect1.w - initialRect1.w) + initialRect1.w),
   1646                     (int) (portion * (targetRect1.h - initialRect1.h) + initialRect1.h));
   1647 
   1648             if (targetRect2 == null) {
   1649                 targetRect2 = new Rect(preview.getX(), preview.getY(),
   1650                         preview.getWidth(), preview.getHeight());
   1651             }
   1652             portion = (now - begin) / (double) DURATION;
   1653             Rect rect2 = new Rect(
   1654                 (int) (portion * (targetRect2.x - initialRect2.x) + initialRect2.x),
   1655                 (int) (portion * (targetRect2.y - initialRect2.y) + initialRect2.y),
   1656                 (int) (portion * (targetRect2.w - initialRect2.w) + initialRect2.w),
   1657                 (int) (portion * (targetRect2.h - initialRect2.h) + initialRect2.h));
   1658 
   1659             gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY));
   1660             gc.drawRectangle(rect1.x, rect1.y, rect1.w, rect1.h);
   1661             gc.drawRectangle(rect2.x, rect2.y, rect2.w, rect2.h);
   1662 
   1663             mCanvas.getDisplay().timerExec(5, this);
   1664         }
   1665 
   1666         @Override
   1667         public void run() {
   1668             mCanvas.redraw();
   1669         }
   1670     }
   1671 
   1672     /**
   1673      * Notifies the {@linkplain RenderPreviewManager} that the configuration used
   1674      * in the main chooser has been changed. This may require updating parent references
   1675      * in the preview configurations inheriting from it.
   1676      *
   1677      * @param oldConfiguration the previous configuration
   1678      * @param newConfiguration the new configuration in the chooser
   1679      */
   1680     public void updateChooserConfig(
   1681             @NonNull Configuration oldConfiguration,
   1682             @NonNull Configuration newConfiguration) {
   1683         if (hasPreviews()) {
   1684             for (RenderPreview preview : mPreviews) {
   1685                 Configuration configuration = preview.getConfiguration();
   1686                 if (configuration instanceof NestedConfiguration) {
   1687                     NestedConfiguration nestedConfig = (NestedConfiguration) configuration;
   1688                     if (nestedConfig.getParent() == oldConfiguration) {
   1689                         nestedConfig.setParent(newConfiguration);
   1690                     }
   1691                 }
   1692             }
   1693         }
   1694     }
   1695 }
   1696