blob: ac657d0c63c94f3d7cfcbac7b90358565661d12c [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.rendering.multi;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.rendering.HardwareConfigHelper;
import com.android.ide.common.rendering.api.Features;
import com.android.ide.common.res2.ResourceItem;
import com.android.ide.common.resources.LocaleManager;
import com.android.ide.common.resources.configuration.*;
import com.android.sdklib.SdkVersionInfo;
import com.android.resources.Density;
import com.android.resources.LayoutDirection;
import com.android.resources.ResourceType;
import com.android.resources.ScreenSize;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.devices.Device;
import com.android.sdklib.devices.Screen;
import com.android.sdklib.devices.State;
import com.android.tools.idea.configurations.*;
import com.android.tools.idea.ddms.screenshot.DeviceArtPainter;
import com.android.tools.idea.model.AndroidModuleInfo;
import com.android.tools.idea.rendering.AppResourceRepository;
import com.android.tools.idea.rendering.Locale;
import com.android.tools.idea.rendering.ResourceHelper;
import com.google.common.collect.Lists;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.xml.XmlFile;
import com.intellij.util.Alarm;
import com.intellij.util.containers.IntArrayList;
import com.intellij.util.ui.Animator;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.List;
import static com.android.tools.idea.configurations.ConfigurationListener.CFG_DEVICE;
import static com.android.tools.idea.configurations.ConfigurationListener.CFG_DEVICE_STATE;
import static com.android.tools.idea.rendering.ShadowPainter.SHADOW_SIZE;
import static com.android.tools.idea.rendering.ShadowPainter.SMALL_SHADOW_SIZE;
import static com.android.tools.idea.rendering.multi.RenderPreviewMode.*;
import static com.intellij.util.Alarm.ThreadToUse.POOLED_THREAD;
import static com.intellij.util.Alarm.ThreadToUse.SWING_THREAD;
/**
* Manager for the configuration previews, which handles layout computations,
* managing the image buffer cache, etc
* <p>
* Missing:
* <ol>
* <li> Support for manual thumbnails </li>
* <li> Support for included layouts </li>
* </ol>
*/
public class RenderPreviewManager implements Disposable {
public static final boolean SUPPORTS_MANUAL_PREVIEWS = false;
static final int VERTICAL_GAP = 18;
static final int HORIZONTAL_GAP = 12;
static final int TITLE_HEIGHT = 14;
private static double ourScale = 1.0;
private static final int RENDER_DELAY = 150;
private static final int MAX_WIDTH = 200;
@SuppressWarnings("SuspiciousNameCombination")
private static final int MAX_HEIGHT = MAX_WIDTH;
private static boolean ZOOM_ENABLED = false;
private static final int ZOOM_ICON_WIDTH = 16;
private static final int ZOOM_ICON_HEIGHT = 16;
private @Nullable List<RenderPreview> myPreviews;
private @NotNull final RenderContext myRenderContext;
private @NotNull RenderPreviewMode myMode = NONE;
private @Nullable RenderPreview myActivePreview;
private @Nullable VirtualFile myCurrentFile;
private @Nullable SwapAnimation myAnimator;
private int myLayoutHeight;
private int myPrevCanvasWidth;
private int myPrevCanvasHeight;
private int myPrevImageWidth;
private int myPrevImageHeight;
private int myPendingRenderCount;
/**
* Last seen state revision in this {@link RenderPreviewManager}. If less
* than {@link #ourRevision}, the previews need to be updated on next exposure
*/
private int myRevision;
/**
* Current global revision count
*/
private static int ourRevision;
private boolean myNeedLayout;
private boolean myNeedRender;
/**
* Whether we should render previews in a background thread or in the Swing thread.
* There have been issues with this in the past, for example running into this
* exception:
* java.lang.IllegalStateException: After scene creation, #init() must be called
* at com.android.layoutlib.bridge.impl.RenderAction.acquire(RenderAction.java:151)
* <p>
* However, it seems to work for now so enabled.
*/
private static final boolean RENDER_ASYNC = true;
@SuppressWarnings("ConstantConditions")
private final @NotNull Alarm myAlarm = RENDER_ASYNC ? new Alarm(POOLED_THREAD, this) : new Alarm(SWING_THREAD, this);
/**
* Creates a {@link RenderPreviewManager} associated with the given canvas
*
* @param canvas the canvas to manage previews for
*/
public RenderPreviewManager(@NotNull RenderContext canvas) {
myRenderContext = canvas;
}
/** Remove mouse listeners for this preview manager for the given component */
public void unregisterMouseListener(@NotNull Component source) {
source.removeMouseListener(myPreviewMouseListener);
source.removeMouseMotionListener(myPreviewMouseListener);
}
/** Add mouse listeners for this preview manager for the given component */
public void registerMouseListener(@NotNull Component source) {
if (myPreviewMouseListener == null) {
myPreviewMouseListener = new MouseAdapter() {
@Override
public void mouseMoved(MouseEvent mouseEvent) {
super.mouseMoved(mouseEvent);
RenderPreviewManager.this.moved(mouseEvent);
}
@Override
public void mouseClicked(MouseEvent mouseEvent) {
super.mouseClicked(mouseEvent);
RenderPreviewManager.this.click(mouseEvent);
}
@Override
public void mouseEntered(MouseEvent mouseEvent) {
super.mouseEntered(mouseEvent);
RenderPreviewManager.this.enter(mouseEvent);
}
@Override
public void mouseExited(MouseEvent mouseEvent) {
super.mouseExited(mouseEvent);
RenderPreviewManager.this.exit(mouseEvent);
}
};
}
source.addMouseListener(myPreviewMouseListener);
source.addMouseMotionListener(myPreviewMouseListener);
}
private MouseAdapter myPreviewMouseListener;
/**
* Revise the global state revision counter. This will cause all layout
* preview managers to refresh themselves to the latest revision when they
* are next exposed.
*/
public static void bumpRevision() {
ourRevision++;
}
/**
* Returns the associated render context
*
* @return the render context
*/
@NotNull
public RenderContext getRenderContext() {
return myRenderContext;
}
/**
* Zooms in (grows all previews)
*/
public void zoomIn() {
if (ZOOM_ENABLED) {
setScale(ourScale * (1 / 0.9));
updatedZoom();
}
}
/**
* Zooms out (shrinks all previews)
*/
public void zoomOut() {
if (ZOOM_ENABLED) {
setScale(ourScale * (0.9 / 1));
updatedZoom();
}
}
/**
* Zooms to 100 (resets zoom)
*/
public void zoomReset() {
if (ZOOM_ENABLED) {
setScale(1.0);
updatedZoom();
myNeedLayout = true;
redraw();
}
}
private static void setScale(double newScale) {
assert ZOOM_ENABLED;
ourScale = newScale;
if (Math.abs(ourScale - 1.0) < 0.0001) {
ourScale = 1.0;
}
}
private void updatedZoom() {
if (ZOOM_ENABLED) {
if (hasPreviews()) {
assert myPreviews != null;
for (RenderPreview preview : myPreviews) {
preview.disposeThumbnail();
}
RenderPreview preview = getStashedPreview();
if (preview != null) {
preview.disposeThumbnail();
}
}
myNeedLayout = myNeedRender = true;
redraw();
}
}
static int getMaxWidth() {
return (int)(ourScale * MAX_WIDTH);
}
static int getMaxHeight() {
return (int)(ourScale * MAX_HEIGHT);
}
static double getScale() {
return ourScale;
}
/**
* Returns whether there are any manual preview items (provided the current
* mode is manual previews
*
* @return true if there are items in the manual preview list
*/
@SuppressWarnings("ConstantConditions")
public boolean hasManualPreviews() {
assert myMode == CUSTOM;
// Not yet implemented; this shouldn't be called
assert !SUPPORTS_MANUAL_PREVIEWS;
return false;
}
/**
* Delete all the previews
*/
@SuppressWarnings("ConstantConditions")
public void deleteManualPreviews() {
disposePreviews();
selectMode(NONE);
myRenderContext.zoomFit(true /* onlyZoomOut */, false /*allowZoomIn*/);
// Not yet implemented; this shouldn't be called
assert !SUPPORTS_MANUAL_PREVIEWS;
}
/**
* Dispose all the previews
*/
public void disposePreviews() {
if (myPreviews != null) {
List<RenderPreview> old = myPreviews;
myPreviews = null;
for (RenderPreview preview : old) {
preview.dispose();
}
}
}
/**
* Deletes the given preview
*
* @param preview the preview to be deleted
*/
@SuppressWarnings("ConstantConditions")
public void deletePreview(@NotNull RenderPreview preview) {
assert myPreviews != null;
RenderPreviewMode.deleteId(preview.getId());
myPreviews.remove(preview);
preview.dispose();
layout(true);
redraw();
if (SUPPORTS_MANUAL_PREVIEWS) {
assert false; // Need to update persistent list here when manual previews is supported
}
}
/**
* Compute the total width required for the previews, including internal padding
*
* @return total width in pixels
*/
public int computePreviewWidth() {
if (hasPreviews()) {
assert myPreviews != null;
int minPreviewWidth = myPreviews.get(0).getLayoutWidth();
for (RenderPreview preview : myPreviews) {
minPreviewWidth = Math.min(minPreviewWidth, preview.getLayoutWidth());
}
if (minPreviewWidth > 0) {
minPreviewWidth += HORIZONTAL_GAP;
minPreviewWidth += SMALL_SHADOW_SIZE;
}
return minPreviewWidth;
}
return 0;
}
/**
* Layout Algorithm. This sets the {@link RenderPreview#getX()} and
* {@link RenderPreview#getY()} coordinates of all the previews. It also
* marks previews as visible or invisible via
* {@link RenderPreview#setVisible(boolean)} according to their position and
* the current visible view port in the layout canvas. Finally, it also sets
* the {@code myLayoutHeight} field, such that the scrollbars can compute the
* right scrolled area, and that scrolling can cause render refreshes on
* views that are made visible.
* <p/>
* This is not a traditional bin packing problem, because the objects to be
* packaged do not have a fixed size; we can scale them up and down in order
* to provide an "optimal" size.
* <p/>
* See http://en.wikipedia.org/wiki/Packing_problem See
* http://en.wikipedia.org/wiki/Bin_packing_problem
*/
void layout(boolean refresh) {
myNeedLayout = false;
if (myPreviews == null || myPreviews.isEmpty()) {
return;
}
Rectangle clientArea = myRenderContext.getClientArea();
Dimension scaledImageSize = myRenderContext.getScaledImageSize();
int scaledImageWidth = scaledImageSize.width;
int scaledImageHeight = scaledImageSize.height;
if (!refresh &&
(scaledImageWidth == myPrevImageWidth &&
scaledImageHeight == myPrevImageHeight &&
clientArea.width == myPrevCanvasWidth &&
clientArea.height == myPrevCanvasHeight)) {
// No change
return;
}
myPrevImageWidth = scaledImageWidth;
myPrevImageHeight = scaledImageHeight;
myPrevCanvasWidth = clientArea.width;
myPrevCanvasHeight = clientArea.height;
beginRenderScheduling();
myLayoutHeight = 0;
RenderContext.UsageType usageType = myRenderContext.getType();
if (!ourClassicLayout && (usageType == RenderContext.UsageType.XML_PREVIEW || usageType == RenderContext.UsageType.LAYOUT_EDITOR)) {
// Quadrant rendering. In XML preview, the "main" rendering isn't
// directly edited, so doesn't need to be nearly as visually prominent;
// instead we subdivide the space equally and size all the thumbnails
// equally
tiledLayout();
}
else if (previewsHaveIdenticalSize() || fixedOrder()) {
// If all the preview boxes are of identical sizes, or if the order is predetermined,
// just lay them out in rows.
rowLayout();
}
else if (previewsFit()) {
layoutFullFit();
}
else {
rowLayout();
}
}
private void tiledLayout() {
assert myPreviews != null;
PreviewTileLayout tileLayout = new PreviewTileLayout(myPreviews, myRenderContext, fixedOrder());
tileLayout.performLayout();
myFixedRenderSize = tileLayout.getFixedRenderSize();
myLayoutHeight = tileLayout.getLayoutHeight();
}
private Dimension myFixedRenderSize;
@Nullable
public Dimension getFixedRenderSize() {
return myFixedRenderSize;
}
void redraw() {
myRenderContext.getComponent().repaint();
}
/**
* Performs a simple layout where the views are laid out in a row, wrapping
* around the top left canvas image.
*/
private void rowLayout() {
assert myPreviews != null;
PreviewRowLayout tileLayout = new PreviewRowLayout(myPreviews, myRenderContext, fixedOrder());
tileLayout.performLayout();
myLayoutHeight = tileLayout.getLayoutHeight();
}
private boolean fixedOrder() {
return myMode == SCREENS;
}
/**
* Returns true if all the previews have the same identical size
*/
private boolean previewsHaveIdenticalSize() {
if (!hasPreviews()) {
return true;
}
assert myPreviews != null;
Iterator<RenderPreview> iterator = myPreviews.iterator();
RenderPreview first = iterator.next();
int width = first.getLayoutWidth();
int height = first.getLayoutHeight();
while (iterator.hasNext()) {
RenderPreview preview = iterator.next();
if (width != preview.getLayoutWidth() || height != preview.getLayoutHeight()) {
return false;
}
}
return true;
}
/**
* Returns true if all the previews can fully fit in the available space
*/
private boolean previewsFit() {
assert myPreviews != null;
Rectangle clientArea = myRenderContext.getClientArea();
Dimension scaledImageSize = myRenderContext.getScaledImageSize();
int scaledImageWidth = scaledImageSize.width;
int scaledImageHeight = scaledImageSize.height;
int availableWidth = clientArea.x + clientArea.width - getX();
int availableHeight = clientArea.y + clientArea.height - getY();
int bottomBorder = scaledImageHeight + SHADOW_SIZE;
int rightHandSide = scaledImageWidth + HORIZONTAL_GAP;
// First see if we can fit everything; if so, we can try to make the layouts
// larger such that they fill up all the available space
long availableArea = rightHandSide * bottomBorder + availableWidth * (Math.max(0, availableHeight - bottomBorder));
long requiredArea = 0;
for (RenderPreview preview : myPreviews) {
// Note: This does not include individual preview scale; the layout
// algorithm itself may be tweaking the scales to fit elements within
// the layout
requiredArea += preview.getArea();
}
return requiredArea * ourScale < availableArea;
}
private void layoutFullFit() {
assert myPreviews != null;
PreviewBinPackingLayout tileLayout = new PreviewBinPackingLayout(myPreviews, myRenderContext, getX(), getY());
if (tileLayout.performLayout()) {
myLayoutHeight = tileLayout.getLayoutHeight();
} else {
rowLayout();
}
}
/**
* Paints the configuration previews
*
* @param gc the graphics context to paint into
*/
public void paint(Graphics2D gc) {
if (hasPreviews()) {
assert myPreviews != null;
// Ensure up to date at all times; consider moving if it's too expensive
layout(myNeedLayout);
if (myNeedRender) {
renderPreviews();
}
int rootX = getX();
int rootY = getY();
Configuration canvasConfiguration = myRenderContext.getConfiguration();
if (canvasConfiguration == null) {
return;
}
Rectangle clientArea = myRenderContext.getClientArea();
for (RenderPreview preview : myPreviews) {
if (preview.isVisible()) {
int x = rootX + preview.getX();
int y = rootY + preview.getY();
gc.setClip(0, 0, clientArea.width, clientArea.height);
preview.paint(gc, x, y);
}
}
/* TODO: Render a render-preview like label over the main rendering
RenderPreview preview = getStashedPreview();
if (preview != null) {
String displayName = null;
Configuration configuration = preview.getConfiguration();
if (configuration instanceof VaryingConfiguration) {
// Use override flags from stashed preview, but configuration
// data from live (not varying) configured configuration
VaryingConfiguration cfg = (VaryingConfiguration)configuration;
int flags = cfg.getAlternateFlags() | cfg.getOverrideFlags();
displayName = NestedConfiguration.computeDisplayName(flags, canvasConfiguration);
}
else if (configuration instanceof NestedConfiguration) {
int flags = ((NestedConfiguration)configuration).getOverrideFlags();
displayName = NestedConfiguration.computeDisplayName(flags, canvasConfiguration);
}
else {
displayName = configuration.getDisplayName();
}
if (displayName != null) {
int x = destX + destWidth / 2 - preview.getWidth() / 2;
int y = destY + destHeight;
preview.paintTitle(gc, x, y, false, displayName);
}
}
*/
}
if (myAnimator != null) {
myAnimator.paint(gc);
}
}
private void addPreview(@NotNull RenderPreview preview) {
String id = preview.getId();
if (id == null) {
id = preview.getDisplayName();
preview.setId(id);
}
if (RenderPreviewMode.isDeletedId(id)) {
return;
}
if (myPreviews == null) {
myPreviews = Lists.newArrayList();
}
myPreviews.add(preview);
}
/**
* Adds the current configuration as a new configuration preview
*/
@SuppressWarnings("ConstantConditions")
public void addAsThumbnail() {
assert !SUPPORTS_MANUAL_PREVIEWS; // Not yet implemented
}
/**
* Computes a unique new name for a configuration preview that represents
* the current, default configuration
*
* @return a unique name
*/
@SuppressWarnings("UnusedDeclaration")
private String getUniqueName() {
if (SUPPORTS_MANUAL_PREVIEWS) {
if (myPreviews != null && !myPreviews.isEmpty()) {
Set<String> names = new HashSet<String>(myPreviews.size());
for (RenderPreview preview : myPreviews) {
names.add(preview.getDisplayName());
}
int index = 2;
while (true) {
String name = String.format("Config%1$d", index);
if (!names.contains(name)) {
return name;
}
index++;
}
}
}
return "Config1";
}
/**
* Generates a bunch of default configuration preview thumbnails
*/
public void addDefaultPreviews() {
Configuration parent = myRenderContext.getConfiguration();
if (parent instanceof NestedConfiguration) {
parent = ((NestedConfiguration)parent).getParent();
}
if (parent == null) {
return;
}
// Create Language variation
createLocaleVariation(parent);
// Vary screen size
// TODO: Be smarter here: Pick a screen that is both as differently as possible
// from the current screen as well as also supported. So consider
// things like supported screens, targetSdk etc.
createScreenVariations(parent);
// Vary orientation
createStateVariation(parent);
// Vary render target
createRenderTargetVariation(parent);
// Also add in include-context previews, if any
// TODO: Implement this
//addIncludedInPreviews();
// Make a placeholder preview for the current screen, in case we switch from it
RenderPreview preview = RenderPreview.create(this, parent, false);
setStashedPreview(preview);
sortPreviewsByOrientation();
}
private void createRenderTargetVariation(@SuppressWarnings("UnusedParameters") @NotNull Configuration parent) {
/* This is disabled for now: need to load multiple versions of layoutlib.
When I did this, there seemed to be some drug interactions between
them, and I would end up with NPEs in layoutlib code which normally works.
VaryingConfiguration configuration =
VaryingConfiguration.create(chooser, parent);
configuration.setAlternatingTarget(true);
configuration.syncFolderConfig();
addPreview(RenderPreview.create(this, configuration));
*/
}
private void createStateVariation(@NotNull Configuration parent) {
State currentState = parent.getDeviceState();
State nextState = parent.getNextDeviceState(currentState);
if (nextState != currentState) {
VaryingConfiguration configuration = VaryingConfiguration.create(parent);
configuration.setAlternateDeviceState(true);
addPreview(RenderPreview.create(this, configuration, false));
}
}
private void createLocaleVariation(@NotNull Configuration parent) {
LocaleQualifier currentLanguage = parent.getLocale().qualifier;
for (Locale locale : parent.getConfigurationManager().getLocales()) {
LocaleQualifier qualifier = locale.qualifier;
if (!qualifier.getLanguage().equals(currentLanguage.getLanguage())) {
VaryingConfiguration configuration = VaryingConfiguration.create(parent);
configuration.setAlternateLocale(true);
addPreview(RenderPreview.create(this, configuration, false));
break;
}
}
}
private void createScreenVariations(@NotNull Configuration parent) {
VaryingConfiguration configuration;
configuration = VaryingConfiguration.create(parent);
configuration.setVariation(0);
configuration.setAlternateDevice(true);
addPreview(RenderPreview.create(this, configuration, false));
configuration = VaryingConfiguration.create(parent);
configuration.setVariation(1);
configuration.setAlternateDevice(true);
addPreview(RenderPreview.create(this, configuration, false));
}
/**
* Returns the current mode as seen by this {@link RenderPreviewManager}.
* Note that it may not yet have been synced with the global mode kept in
* {@link RenderPreviewMode#getCurrent()}.
*
* @return the current preview mode
*/
@NotNull
public RenderPreviewMode getMode() {
return myMode;
}
/**
* Update the set of previews for the current mode
*
* @param force force a refresh even if the preview type has not changed
* @return true if the views were recomputed, false if the previews were
* already showing and the mode not changed
*/
public boolean recomputePreviews(boolean force) {
RenderPreviewMode newMode = getCurrent();
myCurrentFile = myRenderContext.getVirtualFile();
if (newMode == myMode && !force && (myRevision == ourRevision || myMode == NONE || myMode == CUSTOM)) {
return false;
}
myMode = newMode;
myRevision = ourRevision;
if (ZOOM_ENABLED) {
setScale(1.0);
}
disposePreviews();
// Only show device frames when showing screen sizes
myRenderContext.setDeviceFramesEnabled(myMode == NONE || myMode == SCREENS);
switch (myMode) {
case DEFAULT:
addDefaultPreviews();
break;
case INCLUDES:
addIncludedInPreviews();
break;
case LOCALES:
addLocalePreviews();
break;
case RTL:
addRtlPreviews();
break;
case API_LEVELS:
addApiLevelPreviews();
break;
case SCREENS:
addScreenSizePreviews();
break;
case VARIATIONS:
addVariationPreviews();
break;
case CUSTOM:
addManualPreviews();
break;
case NONE:
// Can't just set myNeedZoom because with no previews, the paint
// method does nothing
myRenderContext.zoomFit(false /* onlyZoomOut */, false /*allowZoomIn*/);
myFixedRenderSize = null;
myRenderContext.setMaxSize(0, 0);
break;
default:
assert false : myMode;
}
// We schedule layout for the next redraw rather than process it here immediately;
// not only does this let us avoid doing work for windows where the tab is in the
// background, but when a file is opened we may not know the size of the canvas
// yet, and the layout methods need it in order to do a good job. By the time
// the canvas is painted, we have accurate bounds.
myNeedLayout = myNeedRender = true;
myRenderContext.updateLayout();
layout(true);
redraw();
return true;
}
/**
* Sets the new render preview mode to use
*
* @param mode the new mode
*/
public void selectMode(@NotNull RenderPreviewMode mode) {
if (mode != myMode) {
setCurrent(mode);
recomputePreviews(false);
}
}
/**
* Similar to {@link #addDefaultPreviews()} but for locales
*/
public void addLocalePreviews() {
Configuration parent = myRenderContext.getConfiguration();
if (parent == null) {
return;
}
List<Locale> locales = parent.getConfigurationManager().getLocales();
for (Locale locale : locales) {
if (!locale.hasLanguage() && !locale.hasRegion()) {
continue;
}
NestedConfiguration configuration = NestedConfiguration.create(parent);
configuration.setOverrideLocale(true);
configuration.setLocale(locale);
String displayName = getLocaleLabel(locale);
configuration.setDisplayName(displayName);
addPreview(RenderPreview.create(this, configuration, false));
}
// Make a placeholder preview for the current screen, in case we switch from it
Locale locale = parent.getLocale();
String label = getLocaleLabel(locale);
parent.setDisplayName(label);
RenderPreview preview = RenderPreview.create(this, parent, false);
setStashedPreview(preview);
// No need to sort: they should all be identical
}
private static String getLocaleLabel(Locale locale) {
if (!locale.hasLanguage()) {
return "Default";
}
// Similar to LocaleMenuAction.getLocaleLabel with brief=false, so it includes
// a full language name, but it inlines the region code without mentioning the
// full region name.
String languageCode = locale.qualifier.getLanguage();
String languageName = LocaleManager.getLanguageName(languageCode);
if (!locale.hasRegion()) {
if (languageName != null) {
return String.format("%1$s (%2$s)", languageName, languageCode);
}
else {
return languageCode;
}
}
else {
String regionCode = locale.qualifier.getRegion();
assert regionCode != null; // because hasRegion()
if (languageName != null) {
return String.format("%1$s (%2$s/%3$s)", languageName, languageCode, regionCode);
}
return String.format("%1$s/%2$s", languageCode, regionCode);
}
}
/**
* Adds previews across API levels
*/
public void addApiLevelPreviews() {
// For performance reasons and because older layoutlib levels have a number of bugs not back-ported,
// we *simulate* older API levels by a combination of using a new layout library and
// (1) Picking different themes according to API levels, e.g. Theme classic for API level < 11 and
// Holo for API level > 11
// (2) Tricking the resource resolvers, which produces a full configuration, to believe that it is
// at an older level, such that if it is simulating API level 11 and it sees a -v14 folder, it
// will not consider that a match
// (3) Possibly mutating the computed resources slightly by pointing to older resource files
// when available. E.g. if simulating Froyo, and you have API 8 installed, and a resource
// value points to the ninepatch file for a text field, if we find the same one in API 8 use
// that one instead. If we do this with XML resources we have to be prepared to resolve aliases
// and theme resources from the older platform which seems iffy.
// TODO:
// (4) Rewriting the Build.VERSION.SDK_INT field when loading classes from layoutlib such that it
// returns the right value (for use by custom view logic)
// (5) Revisit my RenderService to make sure choices based on minSdk and target there
// reflect the compatibility render target!
Configuration configuration = myRenderContext.getConfiguration();
if (configuration == null) {
return;
}
IAndroidTarget currentTarget = configuration.getTarget();
if (currentTarget == null) {
return;
}
int currentApi = currentTarget.getVersion().getFeatureLevel();
IAndroidTarget[] targets = configuration.getConfigurationManager().getTargets();
IAndroidTarget highestTarget = null;
for (int i = targets.length - 1; i >= 0; i--) {
IAndroidTarget target = targets[i];
if (ConfigurationManager.isLayoutLibTarget(target)) {
highestTarget = target;
break;
}
}
if (highestTarget == null) {
return;
}
IntArrayList list = new IntArrayList();
Module module = myRenderContext.getModule();
if (module == null) {
return;
}
AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet == null) {
return;
}
AndroidModuleInfo moduleInfo = AndroidModuleInfo.get(facet);
int highestApiLevel = highestTarget.getVersion().getFeatureLevel();
int minSdkVersion = moduleInfo.getMinSdkVersion().getFeatureLevel();
int min = Math.max(8, minSdkVersion);
int max = Math.min(SdkVersionInfo.HIGHEST_KNOWN_API, highestApiLevel);
// Don't attempt to render wear-layouts in older platforms
Device device = configuration.getDevice();
if (HardwareConfigHelper.isWear(device)) {
min = 20;
}
AppResourceRepository resources = AppResourceRepository.getAppResources(facet, true);
XmlFile xmlFile = myRenderContext.getXmlFile();
if (xmlFile == null) {
return;
}
String resourceName = ResourceHelper.getResourceName(xmlFile);
List<ResourceItem> items = resources.getResourceItem(ResourceType.LAYOUT, resourceName);
// Froyo: Doesn't look right when rendered with a newer layoutlib: assets are all wrong.
addIfWithinInclusive(min, max, 8, list, items); // Classic: Froyo
addIfWithinInclusive(min, max, 9, list, items); // Classic: Gingerbread
addIfWithinInclusive(min, max, 15, list, items); // Holo: ICS
if (min > 15 && min < 21) {
// If you set for example minSdkVersion to 17, that's still Holo, and we want to include it
// since 15 wasn't eligible!
addIfWithinInclusive(min, max, min, list, items); // Holo
}
addIfWithinInclusive(min, max, 21, list, items); // Material: LMP
for (int i = 0, n = list.size(); i < n; i++) {
int api = list.get(i);
if (api == currentApi) {
// Show the OTHER API levels
continue;
}
NestedConfiguration apiConfig = NestedConfiguration.create(configuration);
apiConfig.setOverrideTarget(true);
IAndroidTarget realTarget = null;
for (int j = targets.length - 1; j >= 0; j--) {
IAndroidTarget target = targets[j];
if (target.getVersion().getFeatureLevel() == api && ConfigurationManager.isLayoutLibTarget(target)) {
realTarget = target;
break;
}
}
IAndroidTarget target;
if (realTarget == currentTarget) {
target = currentTarget;
} else {
target = new CompatibilityRenderTarget(api > currentApi ? highestTarget : currentTarget, api, realTarget);
}
apiConfig.setTarget(target);
String label = SdkVersionInfo.getCodeName(api);
if (label == null) {
label = Integer.toString(api);
}
apiConfig.setDisplayName(label);
apiConfig.setTheme(null);
RenderPreview preview = RenderPreview.create(this, apiConfig, false);
addPreview(preview);
}
RenderPreview preview = RenderPreview.create(this, configuration, false);
String codeName = SdkVersionInfo.getCodeName(currentTarget.getVersion().getFeatureLevel());
if (codeName == null) {
codeName = currentTarget.getVersion().getApiString();
}
preview.setDisplayName(codeName);
setStashedPreview(preview);
}
private static void addIfWithinInclusive(int min, int max, int value, IntArrayList list, @Nullable List<ResourceItem> items) {
if (value >= min && value <= max) {
// Make sure we have a compatible match for this version for this resources
boolean foundCompatible = true;
if (items != null) {
foundCompatible = false;
// Make sure that if you're looking at a layout only present in say -v14, we don't attempt
// to render this in -v9.
for (ResourceItem item : items) {
FolderConfiguration configuration = item.getConfiguration();
VersionQualifier qualifier = configuration.getVersionQualifier();
if (qualifier == null || qualifier.getVersion() <= value) {
foundCompatible = true;
break;
}
}
}
if (foundCompatible) {
list.add(value);
}
}
}
/**
* Similar to {@link #addDefaultPreviews()} but for right-to-left previews
*/
public void addRtlPreviews() {
Configuration parent = myRenderContext.getConfiguration();
if (parent == null) {
return;
}
LayoutDirectionQualifier layoutDirectionQualifier = parent.getFullConfig().getLayoutDirectionQualifier();
boolean isRtl = layoutDirectionQualifier != null && layoutDirectionQualifier.getValue() == LayoutDirection.RTL;
NestedConfiguration configuration = NestedConfiguration.create(parent);
configuration.getFullConfig().setLayoutDirectionQualifier(new LayoutDirectionQualifier(isRtl ? LayoutDirection.LTR : LayoutDirection.RTL));
String displayName = isRtl ? "LTR" : "RTL";
configuration.setDisplayName(displayName);
addPreview(RenderPreview.create(this, configuration, false));
// Make a placeholder preview for the current screen, in case we switch from it
String label = isRtl ? "RTL" : "LTR";
parent.setDisplayName(label);
RenderPreview preview = RenderPreview.create(this, parent, false);
setStashedPreview(preview);
// No need to sort: they should all identical size
}
/**
* Similar to {@link #addDefaultPreviews()} but for screen sizes
*/
public void addScreenSizePreviews() {
Configuration configuration = myRenderContext.getConfiguration();
if (configuration == null) {
return;
}
List<Device> devices = configuration.getConfigurationManager().getDevices();
boolean canScaleNinePatch = configuration.supports(Features.FIXED_SCALABLE_NINE_PATCH);
// TODO: Only do this if there is no *better* fit for the other orientation
Device currentDevice = configuration.getDevice();
boolean useDefaultState = currentDevice == null || configuration.getDeviceState() == currentDevice.getDefaultState();
if (configuration.getEditedConfig().getScreenOrientationQualifier() != null) {
useDefaultState = false;
}
boolean isWear = HardwareConfigHelper.isWear(currentDevice);
// Rearrange the devices a bit such that the most interesting devices bubble
// to the front
// 10" tablet, 7" tablet, reference phones, tiny phone, and in general the first
// version of each seen screen size
Set<ScreenSize> seenSizes = EnumSet.noneOf(ScreenSize.class);
State currentState = configuration.getDeviceState();
String currentStateName = currentState != null ? currentState.getName() : "";
DeviceArtPainter framePainter = DeviceArtPainter.getInstance();
for (Device device : devices) {
boolean interesting = false;
State state = device.getState(currentStateName);
if (state == null) {
state = device.getAllStates().get(0);
}
if (isWear) {
// If you're viewing a wear device, only show other wear devices as alternative screen sizes
interesting = HardwareConfigHelper.isWear(device);
} else if (HardwareConfigHelper.isNexus(device) && !HardwareConfigHelper.isGeneric(device)) {
interesting = true;
// Skip the older Nexus 7 since having 2 side-by-side isn't very interesting, even if the 2012 and 2013 versions
// have different dpi's
if ("Nexus 7".equals(device.getId())) { // This is the 2012 edition. The 2013 edition has id "Nexus 7 2013".
interesting = false;
}
}
FolderConfiguration c = DeviceConfigHelper.getFolderConfig(state);
if (c != null) {
//noinspection ConstantIfStatement,ConstantConditions
if (false) { // only Nexus devices for now to make list shorter; they span everything from Nexus One up to Nexus 10 so wide range
ScreenSizeQualifier sizeQualifier = c.getScreenSizeQualifier();
if (sizeQualifier != null) {
ScreenSize size = sizeQualifier.getValue();
if (!seenSizes.contains(size)) {
seenSizes.add(size);
interesting = true;
}
}
}
// Omit LDPI, not really used anymore
DensityQualifier density = c.getDensityQualifier();
if (density != null) {
Density d = density.getValue();
if (d == Density.LOW) {
interesting = false;
}
if (!canScaleNinePatch && d != null && !d.isRecommended()) {
interesting = false;
}
}
}
if (interesting) {
// For now only preview items that have dedicated device frames
if (!framePainter.hasDeviceFrame(device)) {
continue;
}
if (device == currentDevice) {
// Show the OTHER devices
continue;
}
NestedConfiguration screenConfig = NestedConfiguration.create(configuration);
screenConfig.setOverrideDevice(true);
screenConfig.setDevice(device, false);
if (useDefaultState) {
screenConfig.setOverrideDeviceState(true);
screenConfig.setDeviceState(device.getDefaultState());
}
String label;
if (HardwareConfigHelper.isNexus(device)) {
// Similar to HardwareConfigHelper#getNexusLabel, but narrower (omits dimensions and density)
String name = device.getDisplayName();
Screen screen = device.getDefaultHardware().getScreen();
float length = (float) screen.getDiagonalLength();
// Round dimensions to the nearest tenth
length = Math.round(10 * length) / 10.0f;
label = String.format(java.util.Locale.US, "%1$s (%2$s\")", name, Float.toString(length));
} else {
label = DeviceMenuAction.getDeviceLabel(device, true);
}
screenConfig.setDisplayName(label);
RenderPreview preview = RenderPreview.create(this, screenConfig, true);
addPreview(preview);
}
}
if (myPreviews != null && myPreviews.size() == 1) {
// If there is only one other device, don't show a label on it; it will be obvious from
// the context and just looks inconsistent (since there is no label on the main device)
myPreviews.get(0).setDisplayName("");
}
RenderPreview preview = RenderPreview.create(this, configuration, true);
setStashedPreview(preview);
// Sorted by screen size, in decreasing order
sortPreviewsByScreenSize();
}
/**
* Previews this layout as included in other layouts
*/
public void addIncludedInPreviews() {
throw new RuntimeException("Not yet implemented");
//ConfigurationChooser chooser = getChooser();
//IProject project = chooser.getProject();
//if (project == null) {
// return;
//}
//IncludeFinder finder = IncludeFinder.get(project);
//
//final List<Reference> includedBy = finder.getIncludedBy(chooser.getEditedFile());
//
//if (includedBy == null || includedBy.isEmpty()) {
// // TODO: Generate some useful defaults, such as including it in a ListView
// // as the list item layout?
// return;
//}
//
//for (final Reference reference : includedBy) {
// String title = reference.getDisplayName();
// Configuration config = Configuration.create(chooser.getConfiguration(), reference.getFile());
// RenderPreview preview = RenderPreview.create(this, config);
// preview.setDisplayName(title);
// preview.setIncludedWithin(reference);
//
// addPreview(preview);
//}
//
//sortPreviewsByOrientation();
}
/**
* Previews this layout as included in other layouts
*/
public void addVariationPreviews() {
Configuration configuration = myRenderContext.getConfiguration();
if (configuration == null) {
return;
}
VirtualFile file = configuration.getFile();
List<VirtualFile> variations = ResourceHelper.getResourceVariations(file, false /*includeSelf*/);
// Sort by parent folder
Collections.sort(variations, new Comparator<VirtualFile>() {
@Override
public int compare(VirtualFile file1, VirtualFile file2) {
return file1.getParent().getName().compareTo(file2.getParent().getName());
}
});
for (VirtualFile variation : variations) {
String title = variation.getParent().getName();
Configuration variationConfiguration = Configuration.create(configuration, variation);
variationConfiguration.setTheme(configuration.getTheme());
variationConfiguration.setActivity(configuration.getActivity());
RenderPreview preview = RenderPreview.create(this, variationConfiguration, false);
preview.setDisplayName(title);
preview.setAlternateInput(variation);
addPreview(preview);
}
sortPreviewsByOrientation();
}
/**
* Previews this layout using a custom configured set of layouts
*/
@SuppressWarnings("ConstantConditions")
public void addManualPreviews() {
// Not yet implemented; this shouldn't be called
assert !SUPPORTS_MANUAL_PREVIEWS;
}
/**
* Notifies that the main configuration has changed.
*
* @param flags the change flags, a bitmask corresponding to the
* {@code CHANGE_} constants in {@link ConfigurationListener}
*/
public void configurationChanged(int flags) {
// Similar to renderPreviews, but only acts on incomplete previews
if (hasPreviews()) {
assert myPreviews != null;
// Do zoomed images first
beginRenderScheduling();
for (RenderPreview preview : myPreviews) {
if (preview.getScale() > 1.2) {
preview.configurationChanged(flags);
}
}
for (RenderPreview preview : myPreviews) {
if (preview.getScale() <= 1.2) {
preview.configurationChanged(flags);
}
}
RenderPreview preview = getStashedPreview();
if (preview != null) {
preview.configurationChanged(flags);
preview.dispose();
}
myNeedLayout = true;
myNeedRender = true;
redraw();
}
}
/**
* Updates the configuration preview thumbnails
*/
public void renderPreviews() {
if (!Comparing.equal(myRenderContext.getVirtualFile(), myCurrentFile)) {
recomputePreviews(true);
return;
}
if (hasPreviews()) {
assert myPreviews != null;
beginRenderScheduling();
myAlarm.cancelAllRequests();
// Process in visual order
ArrayList<RenderPreview> visualOrder = new ArrayList<RenderPreview>(myPreviews);
Collections.sort(visualOrder, RenderPreview.VISUAL_ORDER);
// Do zoomed images first
for (RenderPreview preview : visualOrder) {
if (preview.getScale() > 1.2 && preview.isVisible()) {
scheduleRender(preview);
}
}
// Non-zoomed images
for (RenderPreview preview : visualOrder) {
if (preview.getScale() <= 1.2 && preview.isVisible()) {
scheduleRender(preview);
}
}
}
myNeedRender = false;
if (myClassicLayout != ourClassicLayout) {
setClassicLayout(ourClassicLayout);
}
}
/**
* Reset rendering scheduling. The next render request will be scheduled
* after a single delay unit.
*/
public void beginRenderScheduling() {
myPendingRenderCount = 0;
}
/**
* Schedule rendering the given preview. Each successive call will add an additional
* delay unit to the schedule from the previous {@link #scheduleRender(RenderPreview)}
* call, until {@link #beginRenderScheduling()} is called again.
*
* @param preview the preview to render
*/
public void scheduleRender(@NotNull RenderPreview preview) {
myPendingRenderCount++;
scheduleRender(preview, myPendingRenderCount * RENDER_DELAY);
}
/**
* Schedule rendering the given preview.
*
* @param preview the preview to render
* @param delay the delay to wait before rendering
*/
public void scheduleRender(@NotNull final RenderPreview preview, long delay) {
Runnable pending = preview.getPendingRendering();
if (pending != null) {
myAlarm.cancelRequest(pending);
}
Runnable request = new Runnable() {
@Override
public void run() {
preview.setPendingRendering(null);
preview.updateSize();
preview.renderSync();
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
redraw();
}
});
}
};
preview.setPendingRendering(request);
myAlarm.addRequest(request, delay);
}
/**
* Switch to the given configuration preview
*
* @param preview the preview to switch to
*/
public void switchTo(@NotNull RenderPreview preview) {
assert myPreviews != null;
Configuration originalConfiguration = myRenderContext.getConfiguration();
if (originalConfiguration == null) {
return;
}
VirtualFile input = preview.getAlternateInput();
if (input != null) {
// This switches to the given file, but the file might not have
// an identical configuration to what was shown in the preview.
// For example, while viewing a 10" layout-xlarge file, it might
// show a preview for a 5" version tied to the default layout. If
// you click on it, it will open the default layout file, but it might
// be using a different screen size; any of those that match the
// default layout, say a 3.8".
//
// Thus, we need to also perform a screen size sync first
Configuration configuration = preview.getConfiguration();
if (configuration instanceof NestedConfiguration) {
NestedConfiguration nestedConfig = (NestedConfiguration)configuration;
boolean setSize = nestedConfig.isOverridingDevice();
if (configuration instanceof VaryingConfiguration) {
VaryingConfiguration c = (VaryingConfiguration)configuration;
setSize |= c.isAlternatingDevice();
}
if (setSize) {
ConfigurationManager configurationManager = originalConfiguration.getConfigurationManager();
VirtualFile editedFile = originalConfiguration.getFile();
assert editedFile != null;
configurationManager.syncToVariations(CFG_DEVICE | CFG_DEVICE_STATE, editedFile, configuration, false, false);
}
}
Project project = configuration.getModule().getProject();
OpenFileDescriptor descriptor = new OpenFileDescriptor(project, input, -1);
FileEditorManager.getInstance(project).openEditor(descriptor, true);
// TODO: Ensure that the layout editor is focused?
return;
}
// The new configuration is the configuration which will become the configuration
// in the layout editor's chooser
Configuration previewConfiguration = preview.getConfiguration();
Configuration newConfiguration = previewConfiguration;
if (newConfiguration instanceof NestedConfiguration) {
// Should never use a complementing configuration for the main
// rendering's configuration; instead, create a new configuration
// with a snapshot of the configuration's current values
newConfiguration = previewConfiguration.clone();
// Remap all the previews to be parented to this new copy instead
// of the old one (which is no longer controlled by the chooser)
for (RenderPreview p : myPreviews) {
Configuration configuration = p.getConfiguration();
if (configuration instanceof NestedConfiguration) {
NestedConfiguration nested = (NestedConfiguration)configuration;
nested.setParent(newConfiguration);
}
}
}
// Make a preview for the configuration which *was* showing in the
// chooser up until this point:
RenderPreview newPreview = getStashedPreview();
if (newPreview == null) {
newPreview = RenderPreview.create(this, originalConfiguration, false);
}
// Update its configuration such that it is complementing or inheriting
// from the new chosen configuration
if (previewConfiguration instanceof VaryingConfiguration) {
VaryingConfiguration varying = VaryingConfiguration.create((VaryingConfiguration)previewConfiguration, newConfiguration);
varying.updateDisplayName();
originalConfiguration = varying;
newPreview.setConfiguration(originalConfiguration);
}
else if (previewConfiguration instanceof NestedConfiguration) {
NestedConfiguration nested =
NestedConfiguration.create((NestedConfiguration)previewConfiguration, originalConfiguration, newConfiguration);
nested.setDisplayName(nested.computeDisplayName());
originalConfiguration = nested;
newPreview.setConfiguration(originalConfiguration);
}
// Replace clicked preview with preview of the formerly edited main configuration
// This doesn't work yet because the image overlay has had its image
// replaced by the configuration previews! I should make a list of them
for (int i = 0, n = myPreviews.size(); i < n; i++) {
if (preview == myPreviews.get(i)) {
myPreviews.set(i, newPreview);
break;
}
}
// Stash the corresponding preview (not active) on the canvas so we can
// retrieve it if clicking to some other preview later
setStashedPreview(preview);
preview.setVisible(false);
// Switch to the configuration from the clicked preview (though it's
// most likely a copy, see above)
myRenderContext.setConfiguration(newConfiguration);
myNeedLayout = true;
redraw();
animateTransition(preview, newPreview);
}
private void animateTransition(RenderPreview preview, RenderPreview newPreview) {
myAnimator = new SwapAnimation(preview, newPreview);
myAnimator.resume();
}
/**
* Gets the preview at the given location, or null if none. This is
* currently deeply tied to where things are painted in onPaint().
*/
@Nullable
RenderPreview getPreview(MouseEvent mouseEvent) {
if (hasPreviews()) {
assert myPreviews != null;
int rootX = getX();
int mouseX = mouseEvent.getX();
if (mouseX < rootX) {
return null;
}
int rootY = getY();
int mouseY = mouseEvent.getY();
for (RenderPreview preview : myPreviews) {
int x = rootX + preview.getX();
int y = rootY + preview.getY();
if (mouseX >= x && mouseX <= x + preview.getWidth()) {
if (mouseY >= y && mouseY <= y + preview.getHeight()) {
return preview;
}
}
}
}
return null;
}
private static int getX() {
return 0;
}
private static int getY() {
return 0;
}
private int getZoomX() {
if (ZOOM_ENABLED) {
Rectangle clientArea = myRenderContext.getClientArea();
int x = clientArea.x + clientArea.width - ZOOM_ICON_WIDTH;
return x - 6;
}
return 0;
}
private int getZoomY() {
if (ZOOM_ENABLED) {
Rectangle clientArea = myRenderContext.getClientArea();
return clientArea.y + 5;
}
return 0;
}
/**
* Returns the height of the layout
*
* @return the height
*/
public int getHeight() {
return myLayoutHeight;
}
/**
* Notifies that preview manager that the mouse cursor has moved to the
* given control position within the layout canvas
*
* @param mouseEvent the mouse event
*/
public void moved(MouseEvent mouseEvent) {
RenderPreview hovered = getPreview(mouseEvent);
if (hovered != myActivePreview) {
if (myActivePreview != null) {
myActivePreview.setActive(false);
}
myActivePreview = hovered;
if (myActivePreview != null) {
myActivePreview.setActive(true);
}
redraw();
}
}
/**
* Notifies that preview manager that the mouse cursor has entered the layout canvas
*
* @param mouseEvent the mouse event
*/
public void enter(MouseEvent mouseEvent) {
moved(mouseEvent);
}
/**
* Notifies that preview manager that the mouse cursor has exited the layout canvas
*
* @param mouseEvent the mouse event
*/
@SuppressWarnings("UnusedParameters")
public void exit(MouseEvent mouseEvent) {
if (myActivePreview != null) {
myActivePreview.setActive(false);
}
myActivePreview = null;
redraw();
}
/**
* Process a mouse click, and return true if it was handled by this manager
* (e.g. the click was on a preview)
*
* @param mouseEvent the mouse event
* @return true if the click occurred over a preview and was handled, false otherwise
*/
public boolean click(MouseEvent mouseEvent) {
// Clicked zoom?
int mouseX = mouseEvent.getX();
int mouseY = mouseEvent.getY();
if (ZOOM_ENABLED) {
int x = getZoomX();
if (x > 0) {
if (mouseX >= x && mouseX <= x + ZOOM_ICON_WIDTH) {
int y = getZoomY();
if (mouseY >= y && mouseY <= y + 4 * ZOOM_ICON_HEIGHT) {
if (mouseY < y + ZOOM_ICON_HEIGHT) {
zoomIn();
mouseEvent.consume();
}
else if (mouseY < y + 2 * ZOOM_ICON_HEIGHT) {
zoomOut();
mouseEvent.consume();
}
else if (mouseY < y + 3 * ZOOM_ICON_HEIGHT) {
zoomReset();
mouseEvent.consume();
}
else {
selectMode(NONE);
mouseEvent.consume();
}
return true;
}
}
}
}
RenderPreview preview = getPreview(mouseEvent);
if (preview != null) {
boolean handled = preview.click(mouseX - getX() - preview.getX(), mouseY - getY() - preview.getY());
if (handled) {
// In case layout was performed, there could be a new preview
// under this coordinate now, so make sure it's hover etc
// shows up
moved(mouseEvent);
mouseEvent.consume();
return true;
}
}
return false;
}
/**
* Returns true if there are thumbnail previews
*
* @return true if thumbnails are being shown
*/
public boolean hasPreviews() {
return myPreviews != null && !myPreviews.isEmpty();
}
private void sortPreviewsByScreenSize() {
if (myPreviews != null) {
Collections.sort(myPreviews, new Comparator<RenderPreview>() {
@Override
public int compare(RenderPreview preview1, RenderPreview preview2) {
Configuration config1 = preview1.getConfiguration();
Configuration config2 = preview2.getConfiguration();
Device device1 = config1.getDevice();
Device device2 = config2.getDevice();
if (device1 != null && device2 != null) {
Screen screen1 = device1.getDefaultHardware().getScreen();
Screen screen2 = device2.getDefaultHardware().getScreen();
if (screen1 != null && screen2 != null) {
double delta = screen1.getDiagonalLength() - screen2.getDiagonalLength();
if (delta != 0.0) {
return (int)Math.signum(delta);
}
else {
if (screen1.getPixelDensity() != screen2.getPixelDensity()) {
return screen1.getPixelDensity().compareTo(screen2.getPixelDensity());
}
}
}
}
State state1 = config1.getDeviceState();
State state2 = config2.getDeviceState();
if (state1 != state2 && state1 != null && state2 != null) {
return state1.getName().compareTo(state2.getName());
}
return preview1.getDisplayName().compareTo(preview2.getDisplayName());
}
});
}
}
private void sortPreviewsByOrientation() {
if (myPreviews != null) {
Collections.sort(myPreviews, new Comparator<RenderPreview>() {
@Override
public int compare(RenderPreview preview1, RenderPreview preview2) {
Configuration config1 = preview1.getConfiguration();
Configuration config2 = preview2.getConfiguration();
State state1 = config1.getDeviceState();
State state2 = config2.getDeviceState();
if (state1 != state2 && state1 != null && state2 != null) {
return state1.getName().compareTo(state2.getName());
}
return preview1.getDisplayName().compareTo(preview2.getDisplayName());
}
});
}
}
/**
* Notifies the {@linkplain RenderPreviewManager} that the configuration used
* in the main chooser has been changed. This may require updating parent references
* in the preview configurations inheriting from it.
*
* @param oldConfiguration the previous configuration
* @param newConfiguration the new configuration in the chooser
*/
@SuppressWarnings("UnusedDeclaration")
public void updateMasterConfiguration(@NotNull Configuration oldConfiguration, @NotNull Configuration newConfiguration) {
if (hasPreviews()) {
assert myPreviews != null;
for (RenderPreview preview : myPreviews) {
Configuration configuration = preview.getConfiguration();
if (configuration instanceof NestedConfiguration) {
NestedConfiguration nestedConfig = (NestedConfiguration)configuration;
if (nestedConfig.getParent() == oldConfiguration) {
nestedConfig.setParent(newConfiguration);
}
}
}
}
}
private RenderPreview myStashedPreview;
private void setStashedPreview(RenderPreview preview) {
myStashedPreview = preview;
}
RenderPreview getStashedPreview() {
return myStashedPreview;
}
@Override
public void dispose() {
Disposer.dispose(this);
disposePreviews();
myAlarm.cancelAllRequests();
myAlarm.dispose();
if (myAnimator != null) {
myAnimator.dispose();
myAnimator = null;
}
}
// Debugging only
private static boolean ourClassicLayout = false;
private boolean myClassicLayout = false;
public static void toggleLayoutMode(RenderContext context) {
ourClassicLayout = !ourClassicLayout;
RenderPreviewManager previewManager = context.getPreviewManager(false);
if (previewManager != null && previewManager.myClassicLayout != ourClassicLayout) {
previewManager.setClassicLayout(ourClassicLayout);
}
}
private void setClassicLayout(boolean classicLayout) {
myClassicLayout = classicLayout;
if (classicLayout) {
myFixedRenderSize = null;
myRenderContext.setMaxSize(0, 0);
myRenderContext.zoomFit(false, false);
if (myPreviews != null) {
for (RenderPreview preview : myPreviews) {
preview.setMaxSize(getMaxWidth(), getMaxHeight());
}
}
}
myRenderContext.updateLayout();
layout(true);
redraw();
}
@Nullable
@VisibleForTesting
public List<RenderPreview> getPreviews() {
return myPreviews;
}
/**
* Animation overlay shown briefly after swapping two previews
*/
private class SwapAnimation extends Animator {
private static final int DURATION = 400; // ms
private Rectangle initialRect1;
private Rectangle targetRect1;
private Rectangle initialRect2;
private Rectangle targetRect2;
private RenderPreview preview;
private Rectangle currentRectangle1;
private Rectangle currentRectangle2;
SwapAnimation(RenderPreview preview1, RenderPreview preview2) {
super("Switch Configurations", 16, DURATION, false);
initialRect1 = new Rectangle(preview1.getX(), preview1.getY(), preview1.getWidth(), preview1.getHeight());
// TODO: Also look at vertical alignment of the left hand side image!
Dimension scaledImageSize = myRenderContext.getScaledImageSize();
initialRect2 = new Rectangle(0, 0, scaledImageSize.width, scaledImageSize.height);
preview = preview2;
}
@Override
public void paintNow(final int frame, final int totalFrames, final int cycle) {
if (targetRect1 == null) {
Dimension scaledImageSize = myRenderContext.getScaledImageSize();
targetRect1 = new Rectangle(0, 0, scaledImageSize.width, scaledImageSize.height);
}
double portion = frame / (double) totalFrames;
Rectangle rect1 = new Rectangle((int)(portion * (targetRect1.x - initialRect1.x) + initialRect1.x),
(int)(portion * (targetRect1.y - initialRect1.y) + initialRect1.y),
(int)(portion * (targetRect1.width - initialRect1.width) + initialRect1.width),
(int)(portion * (targetRect1.height - initialRect1.height) + initialRect1.height));
if (targetRect2 == null) {
targetRect2 = new Rectangle(preview.getX(), preview.getY(), preview.getWidth(), preview.getHeight());
}
Rectangle rect2 = new Rectangle((int)(portion * (targetRect2.x - initialRect2.x) + initialRect2.x),
(int)(portion * (targetRect2.y - initialRect2.y) + initialRect2.y),
(int)(portion * (targetRect2.width - initialRect2.width) + initialRect2.width),
(int)(portion * (targetRect2.height - initialRect2.height) + initialRect2.height));
currentRectangle1 = rect1;
currentRectangle2 = rect2;
redraw();
}
private void paint(Graphics gc) {
//noinspection UseJBColor
gc.setColor(Color.DARK_GRAY);
Rectangle rect1 = currentRectangle1;
if (rect1 != null) {
gc.drawRect(rect1.x, rect1.y, rect1.width, rect1.height);
}
Rectangle rect2 = currentRectangle2;
if (rect2 != null) {
gc.drawRect(rect2.x, rect2.y, rect2.width, rect2.height);
}
}
@Override
protected void paintCycleEnd() {
Disposer.dispose(this);
redraw();
}
@Override
public void dispose() {
super.dispose();
myAnimator = null;
}
}
}