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