• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
RenderPreviewManager(@onNull LayoutCanvas canvas)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      */
bumpRevision()134     public static void bumpRevision() {
135         sRevision++;
136     }
137 
138     /**
139      * Returns the associated chooser
140      *
141      * @return the associated chooser
142      */
143     @NonNull
getChooser()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
getCanvas()155     public LayoutCanvas getCanvas() {
156         return mCanvas;
157     }
158 
159     /** Zooms in (grows all previews) */
zoomIn()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) */
zoomOut()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) */
zoomReset()179     public void zoomReset() {
180         sScale = 1.0;
181         updatedZoom();
182         mNeedZoom = mNeedLayout = true;
183         mCanvas.redraw();
184     }
185 
updatedZoom()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 
getMaxWidth()201     static int getMaxWidth() {
202         return (int) (sScale * MAX_WIDTH);
203     }
204 
getMaxHeight()205     static int getMaxHeight() {
206         return (int) (sScale * MAX_HEIGHT);
207     }
208 
getScale()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      */
hasManualPreviews()219     public boolean hasManualPreviews() {
220         assert mMode == CUSTOM;
221         return mManualList != null && !mManualList.isEmpty();
222     }
223 
224     /** Delete all the previews */
deleteManualPreviews()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 */
disposePreviews()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      */
deletePreview(RenderPreview preview)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      */
computePreviewWidth()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      */
layout(boolean refresh)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      */
rowLayout()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 
fixedOrder()450     private boolean fixedOrder() {
451         return mMode == SCREENS;
452     }
453 
454     /** Returns true if all the previews have the same identical size */
previewsHaveIdenticalSize()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 */
previewsFit()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 
layoutFullFit()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      */
paint(GC gc)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 
addPreview(@onNull RenderPreview preview)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 */
addAsThumbnail()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      */
getUniqueName()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 */
addDefaultPreviews()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 
createRenderTargetVariation(ConfigurationChooser chooser, Configuration parent)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 
createStateVariation(ConfigurationChooser chooser, Configuration parent)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 
createLocaleVariation(ConfigurationChooser chooser, Configuration parent)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 
createScreenVariations(Configuration parent)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
getMode()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      */
recomputePreviews(boolean force)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      */
selectMode(@onNull RenderPreviewMode mode)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 */
addLocalePreviews()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 */
addScreenSizePreviews()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      */
addIncludedInPreviews()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      */
addVariationPreviews()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      */
addManualPreviews()1106     public void addManualPreviews() {
1107         if (mManualList == null) {
1108             loadList();
1109         } else {
1110             mPreviews = mManualList.createPreviews(mCanvas);
1111         }
1112     }
1113 
loadList()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 
saveList()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 
rename(ConfigurationDescription description, String newName)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      */
configurationChanged(int flags)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 */
renderPreviews()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      */
beginRenderScheduling()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      */
scheduleRender(@onNull RenderPreview preview)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      */
switchTo(@onNull RenderPreview preview)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      */
getPreview(ControlPoint mousePos)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 
getX()1389     private int getX() {
1390         return mHScale.translate(0);
1391     }
1392 
getY()1393     private int getY() {
1394         return mVScale.translate(0);
1395     }
1396 
getZoomX()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 
getZoomY()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      */
getHeight()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      */
moved(ControlPoint mousePos)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      */
enter(ControlPoint mousePos)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      */
exit(ControlPoint mousePos)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      */
click(ControlPoint mousePos)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      */
hasPreviews()1513     public boolean hasPreviews() {
1514         return mPreviews != null && !mPreviews.isEmpty();
1515     }
1516 
1517 
sortPreviewsByScreenSize()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 
sortPreviewsByOrientation()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
widgetSelected(SelectionEvent e)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
widgetDefaultSelected(SelectionEvent e)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 
SwapAnimation(RenderPreview preview1, RenderPreview preview2)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 
tick(GC gc)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
run()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      */
updateChooserConfig( @onNull Configuration oldConfiguration, @NonNull Configuration newConfiguration)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