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