aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java1333
1 files changed, 1333 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java
new file mode 100644
index 000000000..5621d5f17
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java
@@ -0,0 +1,1333 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_RENDERING;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SMALL_SHADOW_SIZE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.DEFAULT;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.INCLUDES;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.rendering.api.Result;
+import com.android.ide.common.rendering.api.Result.Status;
+import com.android.ide.common.resources.ResourceFile;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.ResourceResolver;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.ScreenOrientationQualifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Locale;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.NestedConfiguration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.VaryingConfiguration;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.io.IFileWrapper;
+import com.android.io.IAbstractFile;
+import com.android.resources.Density;
+import com.android.resources.ResourceType;
+import com.android.resources.ScreenOrientation;
+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.utils.SdkUtils;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.jobs.IJobChangeEvent;
+import org.eclipse.core.runtime.jobs.IJobChangeListener;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.dialogs.InputDialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Region;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.progress.UIJob;
+import org.w3c.dom.Document;
+
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.lang.ref.SoftReference;
+import java.util.Comparator;
+import java.util.Map;
+
+/**
+ * Represents a preview rendering of a given configuration
+ */
+public class RenderPreview implements IJobChangeListener {
+ /** Whether previews should use large shadows */
+ static final boolean LARGE_SHADOWS = false;
+
+ /**
+ * Still doesn't work; get exceptions from layoutlib:
+ * java.lang.IllegalStateException: After scene creation, #init() must be called
+ * at com.android.layoutlib.bridge.impl.RenderAction.acquire(RenderAction.java:151)
+ * <p>
+ * TODO: Investigate.
+ */
+ private static final boolean RENDER_ASYNC = false;
+
+ /**
+ * Height of the toolbar shown over a preview during hover. Needs to be
+ * large enough to accommodate icons below.
+ */
+ private static final int HEADER_HEIGHT = 20;
+
+ /** Whether to dump out rendering failures of the previews to the log */
+ private static final boolean DUMP_RENDER_DIAGNOSTICS = false;
+
+ /** Extra error checking in debug mode */
+ private static final boolean DEBUG = false;
+
+ private static final Image EDIT_ICON;
+ private static final Image ZOOM_IN_ICON;
+ private static final Image ZOOM_OUT_ICON;
+ private static final Image CLOSE_ICON;
+ private static final int EDIT_ICON_WIDTH;
+ private static final int ZOOM_IN_ICON_WIDTH;
+ private static final int ZOOM_OUT_ICON_WIDTH;
+ private static final int CLOSE_ICON_WIDTH;
+ static {
+ ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
+ IconFactory icons = IconFactory.getInstance();
+ CLOSE_ICON = sharedImages.getImage(ISharedImages.IMG_ETOOL_DELETE);
+ EDIT_ICON = icons.getIcon("editPreview"); //$NON-NLS-1$
+ ZOOM_IN_ICON = icons.getIcon("zoomplus"); //$NON-NLS-1$
+ ZOOM_OUT_ICON = icons.getIcon("zoomminus"); //$NON-NLS-1$
+ CLOSE_ICON_WIDTH = CLOSE_ICON.getImageData().width;
+ EDIT_ICON_WIDTH = EDIT_ICON.getImageData().width;
+ ZOOM_IN_ICON_WIDTH = ZOOM_IN_ICON.getImageData().width;
+ ZOOM_OUT_ICON_WIDTH = ZOOM_OUT_ICON.getImageData().width;
+ }
+
+ /** The configuration being previewed */
+ private @NonNull Configuration mConfiguration;
+
+ /** Configuration to use if we have an alternate input to be rendered */
+ private @NonNull Configuration mAlternateConfiguration;
+
+ /** The associated manager */
+ private final @NonNull RenderPreviewManager mManager;
+ private final @NonNull LayoutCanvas mCanvas;
+
+ private @NonNull SoftReference<ResourceResolver> mResourceResolver =
+ new SoftReference<ResourceResolver>(null);
+ private @Nullable Job mJob;
+ private @Nullable Image mThumbnail;
+ private @Nullable String mDisplayName;
+ private int mWidth;
+ private int mHeight;
+ private int mX;
+ private int mY;
+ private int mTitleHeight;
+ private double mScale = 1.0;
+ private double mAspectRatio;
+
+ /** If non null, points to a separate file containing the source */
+ private @Nullable IFile mAlternateInput;
+
+ /** If included within another layout, the name of that outer layout */
+ private @Nullable Reference mIncludedWithin;
+
+ /** Whether the mouse is actively hovering over this preview */
+ private boolean mActive;
+
+ /**
+ * Whether this preview cannot be rendered because of a model error - such
+ * as an invalid configuration, a missing resource, an error in the XML
+ * markup, etc. If non null, contains the error message (or a blank string
+ * if not known), and null if the render was successful.
+ */
+ private String mError;
+
+ /** Whether in the current layout, this preview is visible */
+ private boolean mVisible;
+
+ /** Whether the configuration has changed and needs to be refreshed the next time
+ * this preview made visible. This corresponds to the change flags in
+ * {@link ConfigurationClient}. */
+ private int mDirty;
+
+ /**
+ * Creates a new {@linkplain RenderPreview}
+ *
+ * @param manager the manager
+ * @param canvas canvas where preview is painted
+ * @param configuration the associated configuration
+ * @param width the initial width to use for the preview
+ * @param height the initial height to use for the preview
+ */
+ private RenderPreview(
+ @NonNull RenderPreviewManager manager,
+ @NonNull LayoutCanvas canvas,
+ @NonNull Configuration configuration) {
+ mManager = manager;
+ mCanvas = canvas;
+ mConfiguration = configuration;
+ updateSize();
+
+ // Should only attempt to create configurations for fully configured devices
+ assert mConfiguration.getDevice() != null
+ && mConfiguration.getDeviceState() != null
+ && mConfiguration.getLocale() != null
+ && mConfiguration.getTarget() != null
+ && mConfiguration.getTheme() != null
+ && mConfiguration.getFullConfig() != null
+ && mConfiguration.getFullConfig().getScreenSizeQualifier() != null :
+ mConfiguration;
+ }
+
+ /**
+ * Sets the configuration to use for this preview
+ *
+ * @param configuration the new configuration
+ */
+ public void setConfiguration(@NonNull Configuration configuration) {
+ mConfiguration = configuration;
+ }
+
+ /**
+ * Gets the scale being applied to the thumbnail
+ *
+ * @return the scale being applied to the thumbnail
+ */
+ public double getScale() {
+ return mScale;
+ }
+
+ /**
+ * Sets the scale to apply to the thumbnail
+ *
+ * @param scale the factor to scale the thumbnail picture by
+ */
+ public void setScale(double scale) {
+ disposeThumbnail();
+ mScale = scale;
+ }
+
+ /**
+ * Returns the aspect ratio of this render preview
+ *
+ * @return the aspect ratio
+ */
+ public double getAspectRatio() {
+ return mAspectRatio;
+ }
+
+ /**
+ * Returns whether the preview is actively hovered
+ *
+ * @return whether the mouse is hovering over the preview
+ */
+ public boolean isActive() {
+ return mActive;
+ }
+
+ /**
+ * Sets whether the preview is actively hovered
+ *
+ * @param active if the mouse is hovering over the preview
+ */
+ public void setActive(boolean active) {
+ mActive = active;
+ }
+
+ /**
+ * Returns whether the preview is visible. Previews that are off
+ * screen are typically marked invisible during layout, which means we don't
+ * have to expend effort computing preview thumbnails etc
+ *
+ * @return true if the preview is visible
+ */
+ public boolean isVisible() {
+ return mVisible;
+ }
+
+ /**
+ * Returns whether this preview represents a forked layout
+ *
+ * @return true if this preview represents a separate file
+ */
+ public boolean isForked() {
+ return mAlternateInput != null || mIncludedWithin != null;
+ }
+
+ /**
+ * Returns the file to be used for this preview, or null if this is not a
+ * forked layout meaning that the file is the one used in the chooser
+ *
+ * @return the file or null for non-forked layouts
+ */
+ @Nullable
+ public IFile getAlternateInput() {
+ if (mAlternateInput != null) {
+ return mAlternateInput;
+ } else if (mIncludedWithin != null) {
+ return mIncludedWithin.getFile();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the area of this render preview, PRIOR to scaling
+ *
+ * @return the area (width times height without scaling)
+ */
+ int getArea() {
+ return mWidth * mHeight;
+ }
+
+ /**
+ * Sets whether the preview is visible. Previews that are off
+ * screen are typically marked invisible during layout, which means we don't
+ * have to expend effort computing preview thumbnails etc
+ *
+ * @param visible whether this preview is visible
+ */
+ public void setVisible(boolean visible) {
+ if (visible != mVisible) {
+ mVisible = visible;
+ if (mVisible) {
+ if (mDirty != 0) {
+ // Just made the render preview visible:
+ configurationChanged(mDirty); // schedules render
+ } else {
+ updateForkStatus();
+ mManager.scheduleRender(this);
+ }
+ } else {
+ dispose();
+ }
+ }
+ }
+
+ /**
+ * Sets the layout position relative to the top left corner of the preview
+ * area, in control coordinates
+ */
+ void setPosition(int x, int y) {
+ mX = x;
+ mY = y;
+ }
+
+ /**
+ * Gets the layout X position relative to the top left corner of the preview
+ * area, in control coordinates
+ */
+ int getX() {
+ return mX;
+ }
+
+ /**
+ * Gets the layout Y position relative to the top left corner of the preview
+ * area, in control coordinates
+ */
+ int getY() {
+ return mY;
+ }
+
+ /** Determine whether this configuration has a better match in a different layout file */
+ private void updateForkStatus() {
+ ConfigurationChooser chooser = mManager.getChooser();
+ FolderConfiguration config = mConfiguration.getFullConfig();
+ if (mAlternateInput != null && chooser.isBestMatchFor(mAlternateInput, config)) {
+ return;
+ }
+
+ mAlternateInput = null;
+ IFile editedFile = chooser.getEditedFile();
+ if (editedFile != null) {
+ if (!chooser.isBestMatchFor(editedFile, config)) {
+ ProjectResources resources = chooser.getResources();
+ if (resources != null) {
+ ResourceFile best = resources.getMatchingFile(editedFile.getName(),
+ ResourceType.LAYOUT, config);
+ if (best != null) {
+ IAbstractFile file = best.getFile();
+ if (file instanceof IFileWrapper) {
+ mAlternateInput = ((IFileWrapper) file).getIFile();
+ } else if (file instanceof File) {
+ mAlternateInput = AdtUtils.fileToIFile(((File) file));
+ }
+ }
+ }
+ if (mAlternateInput != null) {
+ mAlternateConfiguration = Configuration.create(mConfiguration,
+ mAlternateInput);
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a new {@linkplain RenderPreview}
+ *
+ * @param manager the manager
+ * @param configuration the associated configuration
+ * @return a new configuration
+ */
+ @NonNull
+ public static RenderPreview create(
+ @NonNull RenderPreviewManager manager,
+ @NonNull Configuration configuration) {
+ LayoutCanvas canvas = manager.getCanvas();
+ return new RenderPreview(manager, canvas, configuration);
+ }
+
+ /**
+ * Throws away this preview: cancels any pending rendering jobs and disposes
+ * of image resources etc
+ */
+ public void dispose() {
+ disposeThumbnail();
+
+ if (mJob != null) {
+ mJob.cancel();
+ mJob = null;
+ }
+ }
+
+ /** Disposes the thumbnail rendering. */
+ void disposeThumbnail() {
+ if (mThumbnail != null) {
+ mThumbnail.dispose();
+ mThumbnail = null;
+ }
+ }
+
+ /**
+ * Returns the display name of this preview
+ *
+ * @return the name of the preview
+ */
+ @NonNull
+ public String getDisplayName() {
+ if (mDisplayName == null) {
+ String displayName = getConfiguration().getDisplayName();
+ if (displayName == null) {
+ // No display name: this must be the configuration used by default
+ // for the view which is originally displayed (before adding thumbnails),
+ // and you've switched away to something else; now we need to display a name
+ // for this original configuration. For now, just call it "Original"
+ return "Original";
+ }
+
+ return displayName;
+ }
+
+ return mDisplayName;
+ }
+
+ /**
+ * Sets the display name of this preview. By default, the display name is
+ * the display name of the configuration, but it can be overridden by calling
+ * this setter (which only sets the preview name, without editing the configuration.)
+ *
+ * @param displayName the new display name
+ */
+ public void setDisplayName(@NonNull String displayName) {
+ mDisplayName = displayName;
+ }
+
+ /**
+ * Sets an inclusion context to use for this layout, if any. This will render
+ * the configuration preview as the outer layout with the current layout
+ * embedded within.
+ *
+ * @param includedWithin a reference to a layout which includes this one
+ */
+ public void setIncludedWithin(Reference includedWithin) {
+ mIncludedWithin = includedWithin;
+ }
+
+ /**
+ * Request a new render after the given delay
+ *
+ * @param delay the delay to wait before starting the render job
+ */
+ public void render(long delay) {
+ Job job = mJob;
+ if (job != null) {
+ job.cancel();
+ }
+ if (RENDER_ASYNC) {
+ job = new AsyncRenderJob();
+ } else {
+ job = new RenderJob();
+ }
+ job.schedule(delay);
+ job.addJobChangeListener(this);
+ mJob = job;
+ }
+
+ /** Render immediately */
+ private void renderSync() {
+ GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ if (editor.getReadyLayoutLib(false /*displayError*/) == null) {
+ // Don't attempt to render when there is no ready layout library: most likely
+ // the targets are loading/reloading.
+ return;
+ }
+
+ disposeThumbnail();
+
+ Configuration configuration =
+ mAlternateInput != null && mAlternateConfiguration != null
+ ? mAlternateConfiguration : mConfiguration;
+ ResourceResolver resolver = getResourceResolver(configuration);
+ RenderService renderService = RenderService.create(editor, configuration, resolver);
+
+ if (mIncludedWithin != null) {
+ renderService.setIncludedWithin(mIncludedWithin);
+ }
+
+ if (mAlternateInput != null) {
+ IAndroidTarget target = editor.getRenderingTarget();
+ AndroidTargetData data = null;
+ if (target != null) {
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ data = sdk.getTargetData(target);
+ }
+ }
+
+ // Construct UI model from XML
+ DocumentDescriptor documentDescriptor;
+ if (data == null) {
+ documentDescriptor = new DocumentDescriptor("temp", null);//$NON-NLS-1$
+ } else {
+ documentDescriptor = data.getLayoutDescriptors().getDescriptor();
+ }
+ UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode();
+ model.setEditor(mCanvas.getEditorDelegate().getEditor());
+ model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider());
+
+ Document document = DomUtilities.getDocument(mAlternateInput);
+ if (document == null) {
+ mError = "No document";
+ createErrorThumbnail();
+ return;
+ }
+ model.loadFromXmlNode(document);
+ renderService.setModel(model);
+ } else {
+ renderService.setModel(editor.getModel());
+ }
+ RenderLogger log = editor.createRenderLogger(getDisplayName());
+ renderService.setLog(log);
+ RenderSession session = renderService.createRenderSession();
+ Result render = session.render(1000);
+
+ if (DUMP_RENDER_DIAGNOSTICS) {
+ if (log.hasProblems() || !render.isSuccess()) {
+ AdtPlugin.log(IStatus.ERROR, "Found problems rendering preview "
+ + getDisplayName() + ": "
+ + render.getErrorMessage() + " : "
+ + log.getProblems(false));
+ Throwable exception = render.getException();
+ if (exception != null) {
+ AdtPlugin.log(exception, "Failure rendering preview " + getDisplayName());
+ }
+ }
+ }
+
+ if (render.isSuccess()) {
+ mError = null;
+ } else {
+ mError = render.getErrorMessage();
+ if (mError == null) {
+ mError = "";
+ }
+ }
+
+ if (render.getStatus() == Status.ERROR_TIMEOUT) {
+ // TODO: Special handling? schedule update again later
+ return;
+ }
+ if (render.isSuccess()) {
+ BufferedImage image = session.getImage();
+ if (image != null) {
+ createThumbnail(image);
+ }
+ }
+
+ if (mError != null) {
+ createErrorThumbnail();
+ }
+ }
+
+ private ResourceResolver getResourceResolver(Configuration configuration) {
+ ResourceResolver resourceResolver = mResourceResolver.get();
+ if (resourceResolver != null) {
+ return resourceResolver;
+ }
+
+ GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ String theme = configuration.getTheme();
+ if (theme == null) {
+ return null;
+ }
+
+ Map<ResourceType, Map<String, ResourceValue>> configuredFrameworkRes = null;
+ Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes = null;
+
+ FolderConfiguration config = configuration.getFullConfig();
+ IAndroidTarget target = graphicalEditor.getRenderingTarget();
+ ResourceRepository frameworkRes = null;
+ if (target != null) {
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk == null) {
+ return null;
+ }
+ AndroidTargetData data = sdk.getTargetData(target);
+
+ if (data != null) {
+ // TODO: SHARE if possible
+ frameworkRes = data.getFrameworkResources();
+ configuredFrameworkRes = frameworkRes.getConfiguredResources(config);
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ assert configuredFrameworkRes != null;
+
+
+ // get the resources of the file's project.
+ ProjectResources projectRes = ResourceManager.getInstance().getProjectResources(
+ graphicalEditor.getProject());
+ configuredProjectRes = projectRes.getConfiguredResources(config);
+
+ if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
+ if (frameworkRes.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + theme)) {
+ theme = ANDROID_STYLE_RESOURCE_PREFIX + theme;
+ } else {
+ theme = STYLE_RESOURCE_PREFIX + theme;
+ }
+ }
+
+ resourceResolver = ResourceResolver.create(
+ configuredProjectRes, configuredFrameworkRes,
+ ResourceHelper.styleToTheme(theme),
+ ResourceHelper.isProjectStyle(theme));
+ mResourceResolver = new SoftReference<ResourceResolver>(resourceResolver);
+ return resourceResolver;
+ }
+
+ /**
+ * Sets the new image of the preview and generates a thumbnail
+ *
+ * @param image the full size image
+ */
+ void createThumbnail(BufferedImage image) {
+ if (image == null) {
+ mThumbnail = null;
+ return;
+ }
+
+ ImageOverlay imageOverlay = mCanvas.getImageOverlay();
+ boolean drawShadows = imageOverlay == null || imageOverlay.getShowDropShadow();
+ double scale = getWidth() / (double) image.getWidth();
+ int shadowSize;
+ if (LARGE_SHADOWS) {
+ shadowSize = drawShadows ? SHADOW_SIZE : 0;
+ } else {
+ shadowSize = drawShadows ? SMALL_SHADOW_SIZE : 0;
+ }
+ if (scale < 1.0) {
+ if (LARGE_SHADOWS) {
+ image = ImageUtils.scale(image, scale, scale,
+ shadowSize, shadowSize);
+ if (drawShadows) {
+ ImageUtils.drawRectangleShadow(image, 0, 0,
+ image.getWidth() - shadowSize,
+ image.getHeight() - shadowSize);
+ }
+ } else {
+ image = ImageUtils.scale(image, scale, scale,
+ shadowSize, shadowSize);
+ if (drawShadows) {
+ ImageUtils.drawSmallRectangleShadow(image, 0, 0,
+ image.getWidth() - shadowSize,
+ image.getHeight() - shadowSize);
+ }
+ }
+ }
+
+ mThumbnail = SwtUtils.convertToSwt(mCanvas.getDisplay(), image,
+ true /* transferAlpha */, -1);
+ }
+
+ void createErrorThumbnail() {
+ int shadowSize = LARGE_SHADOWS ? SHADOW_SIZE : SMALL_SHADOW_SIZE;
+ int width = getWidth();
+ int height = getHeight();
+ BufferedImage image = new BufferedImage(width + shadowSize, height + shadowSize,
+ BufferedImage.TYPE_INT_ARGB);
+
+ Graphics2D g = image.createGraphics();
+ g.setColor(new java.awt.Color(0xfffbfcc6));
+ g.fillRect(0, 0, width, height);
+
+ g.dispose();
+
+ ImageOverlay imageOverlay = mCanvas.getImageOverlay();
+ boolean drawShadows = imageOverlay == null || imageOverlay.getShowDropShadow();
+ if (drawShadows) {
+ if (LARGE_SHADOWS) {
+ ImageUtils.drawRectangleShadow(image, 0, 0,
+ image.getWidth() - SHADOW_SIZE,
+ image.getHeight() - SHADOW_SIZE);
+ } else {
+ ImageUtils.drawSmallRectangleShadow(image, 0, 0,
+ image.getWidth() - SMALL_SHADOW_SIZE,
+ image.getHeight() - SMALL_SHADOW_SIZE);
+ }
+ }
+
+ mThumbnail = SwtUtils.convertToSwt(mCanvas.getDisplay(), image,
+ true /* transferAlpha */, -1);
+ }
+
+ private static double getScale(int width, int height) {
+ int maxWidth = RenderPreviewManager.getMaxWidth();
+ int maxHeight = RenderPreviewManager.getMaxHeight();
+ if (width > 0 && height > 0
+ && (width > maxWidth || height > maxHeight)) {
+ if (width >= height) { // landscape
+ return maxWidth / (double) width;
+ } else { // portrait
+ return maxHeight / (double) height;
+ }
+ }
+
+ return 1.0;
+ }
+
+ /**
+ * Returns the width of the preview, in pixels
+ *
+ * @return the width in pixels
+ */
+ public int getWidth() {
+ return (int) (mWidth * mScale * RenderPreviewManager.getScale());
+ }
+
+ /**
+ * Returns the height of the preview, in pixels
+ *
+ * @return the height in pixels
+ */
+ public int getHeight() {
+ return (int) (mHeight * mScale * RenderPreviewManager.getScale());
+ }
+
+ /**
+ * Handles clicks within the preview (x and y are positions relative within the
+ * preview
+ *
+ * @param x the x coordinate within the preview where the click occurred
+ * @param y the y coordinate within the preview where the click occurred
+ * @return true if this preview handled (and therefore consumed) the click
+ */
+ public boolean click(int x, int y) {
+ if (y >= mTitleHeight && y < mTitleHeight + HEADER_HEIGHT) {
+ int left = 0;
+ left += CLOSE_ICON_WIDTH;
+ if (x <= left) {
+ // Delete
+ mManager.deletePreview(this);
+ return true;
+ }
+ left += ZOOM_IN_ICON_WIDTH;
+ if (x <= left) {
+ // Zoom in
+ mScale = mScale * (1 / 0.5);
+ if (Math.abs(mScale-1.0) < 0.0001) {
+ mScale = 1.0;
+ }
+
+ render(0);
+ mManager.layout(true);
+ mCanvas.redraw();
+ return true;
+ }
+ left += ZOOM_OUT_ICON_WIDTH;
+ if (x <= left) {
+ // Zoom out
+ mScale = mScale * (0.5 / 1);
+ if (Math.abs(mScale-1.0) < 0.0001) {
+ mScale = 1.0;
+ }
+ render(0);
+
+ mManager.layout(true);
+ mCanvas.redraw();
+ return true;
+ }
+ left += EDIT_ICON_WIDTH;
+ if (x <= left) {
+ // Edit. For now, just rename
+ InputDialog d = new InputDialog(
+ AdtPlugin.getShell(),
+ "Rename Preview", // title
+ "Name:",
+ getDisplayName(),
+ null);
+ if (d.open() == Window.OK) {
+ String newName = d.getValue();
+ mConfiguration.setDisplayName(newName);
+ if (mDescription != null) {
+ mManager.rename(mDescription, newName);
+ }
+ mCanvas.redraw();
+ }
+
+ return true;
+ }
+
+ // Clicked anywhere else on header
+ // Perhaps open Edit dialog here?
+ }
+
+ mManager.switchTo(this);
+ return true;
+ }
+
+ /**
+ * Paints the preview at the given x/y position
+ *
+ * @param gc the graphics context to paint it into
+ * @param x the x coordinate to paint the preview at
+ * @param y the y coordinate to paint the preview at
+ */
+ void paint(GC gc, int x, int y) {
+ mTitleHeight = paintTitle(gc, x, y, true /*showFile*/);
+ y += mTitleHeight;
+ y += 2;
+
+ int width = getWidth();
+ int height = getHeight();
+ if (mThumbnail != null && mError == null) {
+ gc.drawImage(mThumbnail, x, y);
+
+ if (mActive) {
+ int oldWidth = gc.getLineWidth();
+ gc.setLineWidth(3);
+ gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_LIST_SELECTION));
+ gc.drawRectangle(x - 1, y - 1, width + 2, height + 2);
+ gc.setLineWidth(oldWidth);
+ }
+ } else if (mError != null) {
+ if (mThumbnail != null) {
+ gc.drawImage(mThumbnail, x, y);
+ } else {
+ gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER));
+ gc.drawRectangle(x, y, width, height);
+ }
+
+ gc.setClipping(x, y, width, height);
+ Image icon = IconFactory.getInstance().getIcon("renderError"); //$NON-NLS-1$
+ ImageData data = icon.getImageData();
+ int prevAlpha = gc.getAlpha();
+ int alpha = 96;
+ if (mThumbnail != null) {
+ alpha -= 32;
+ }
+ gc.setAlpha(alpha);
+ gc.drawImage(icon, x + (width - data.width) / 2, y + (height - data.height) / 2);
+
+ String msg = mError;
+ Density density = mConfiguration.getDensity();
+ if (density == Density.TV || density == Density.LOW) {
+ msg = "Broken rendering library; unsupported DPI. Try using the SDK manager " +
+ "to get updated layout libraries.";
+ }
+ int charWidth = gc.getFontMetrics().getAverageCharWidth();
+ int charsPerLine = (width - 10) / charWidth;
+ msg = SdkUtils.wrap(msg, charsPerLine, null);
+ gc.setAlpha(255);
+ gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_BLACK));
+ gc.drawText(msg, x + 5, y + HEADER_HEIGHT, true);
+ gc.setAlpha(prevAlpha);
+ gc.setClipping((Region) null);
+ } else {
+ gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER));
+ gc.drawRectangle(x, y, width, height);
+
+ Image icon = IconFactory.getInstance().getIcon("refreshPreview"); //$NON-NLS-1$
+ ImageData data = icon.getImageData();
+ int prevAlpha = gc.getAlpha();
+ gc.setAlpha(96);
+ gc.drawImage(icon, x + (width - data.width) / 2,
+ y + (height - data.height) / 2);
+ gc.setAlpha(prevAlpha);
+ }
+
+ if (mActive) {
+ int left = x ;
+ int prevAlpha = gc.getAlpha();
+ gc.setAlpha(208);
+ Color bg = mCanvas.getDisplay().getSystemColor(SWT.COLOR_WHITE);
+ gc.setBackground(bg);
+ gc.fillRectangle(left, y, x + width - left, HEADER_HEIGHT);
+ gc.setAlpha(prevAlpha);
+
+ y += 2;
+
+ // Paint icons
+ gc.drawImage(CLOSE_ICON, left, y);
+ left += CLOSE_ICON_WIDTH;
+
+ gc.drawImage(ZOOM_IN_ICON, left, y);
+ left += ZOOM_IN_ICON_WIDTH;
+
+ gc.drawImage(ZOOM_OUT_ICON, left, y);
+ left += ZOOM_OUT_ICON_WIDTH;
+
+ gc.drawImage(EDIT_ICON, left, y);
+ left += EDIT_ICON_WIDTH;
+ }
+ }
+
+ /**
+ * Paints the preview title at the given position (and returns the required
+ * height)
+ *
+ * @param gc the graphics context to paint into
+ * @param x the left edge of the preview rectangle
+ * @param y the top edge of the preview rectangle
+ */
+ private int paintTitle(GC gc, int x, int y, boolean showFile) {
+ String displayName = getDisplayName();
+ return paintTitle(gc, x, y, showFile, displayName);
+ }
+
+ /**
+ * Paints the preview title at the given position (and returns the required
+ * height)
+ *
+ * @param gc the graphics context to paint into
+ * @param x the left edge of the preview rectangle
+ * @param y the top edge of the preview rectangle
+ * @param displayName the title string to be used
+ */
+ int paintTitle(GC gc, int x, int y, boolean showFile, String displayName) {
+ int titleHeight = 0;
+
+ if (showFile && mIncludedWithin != null) {
+ if (mManager.getMode() != INCLUDES) {
+ displayName = "<include>";
+ } else {
+ // Skip: just paint footer instead
+ displayName = null;
+ }
+ }
+
+ int width = getWidth();
+ int labelTop = y + 1;
+ gc.setClipping(x, labelTop, width, 100);
+
+ // Use font height rather than extent height since we want two adjacent
+ // previews (which may have different display names and therefore end
+ // up with slightly different extent heights) to have identical title
+ // heights such that they are aligned identically
+ int fontHeight = gc.getFontMetrics().getHeight();
+
+ if (displayName != null && displayName.length() > 0) {
+ gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_WHITE));
+ Point extent = gc.textExtent(displayName);
+ int labelLeft = Math.max(x, x + (width - extent.x) / 2);
+ Image icon = null;
+ Locale locale = mConfiguration.getLocale();
+ if (locale != null && (locale.hasLanguage() || locale.hasRegion())
+ && (!(mConfiguration instanceof NestedConfiguration)
+ || ((NestedConfiguration) mConfiguration).isOverridingLocale())) {
+ icon = locale.getFlagImage();
+ }
+
+ if (icon != null) {
+ int flagWidth = icon.getImageData().width;
+ int flagHeight = icon.getImageData().height;
+ labelLeft = Math.max(x + flagWidth / 2, labelLeft);
+ gc.drawImage(icon, labelLeft - flagWidth / 2 - 1, labelTop);
+ labelLeft += flagWidth / 2 + 1;
+ gc.drawText(displayName, labelLeft,
+ labelTop - (extent.y - flagHeight) / 2, true);
+ } else {
+ gc.drawText(displayName, labelLeft, labelTop, true);
+ }
+
+ labelTop += extent.y;
+ titleHeight += fontHeight;
+ }
+
+ if (showFile && (mAlternateInput != null || mIncludedWithin != null)) {
+ // Draw file flag, and parent folder name
+ IFile file = mAlternateInput != null
+ ? mAlternateInput : mIncludedWithin.getFile();
+ String fileName = file.getParent().getName() + File.separator
+ + file.getName();
+ Point extent = gc.textExtent(fileName);
+ Image icon = IconFactory.getInstance().getIcon("android_file"); //$NON-NLS-1$
+ int flagWidth = icon.getImageData().width;
+ int flagHeight = icon.getImageData().height;
+
+ int labelLeft = Math.max(x, x + (width - extent.x - flagWidth - 1) / 2);
+
+ gc.drawImage(icon, labelLeft, labelTop);
+
+ gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY));
+ labelLeft += flagWidth + 1;
+ labelTop -= (extent.y - flagHeight) / 2;
+ gc.drawText(fileName, labelLeft, labelTop, true);
+
+ titleHeight += Math.max(titleHeight, icon.getImageData().height);
+ }
+
+ gc.setClipping((Region) null);
+
+ return titleHeight;
+ }
+
+ /**
+ * Notifies that the preview's configuration has changed.
+ *
+ * @param flags the change flags, a bitmask corresponding to the
+ * {@code CHANGE_} constants in {@link ConfigurationClient}
+ */
+ public void configurationChanged(int flags) {
+ if (!mVisible) {
+ mDirty |= flags;
+ return;
+ }
+
+ if ((flags & MASK_RENDERING) != 0) {
+ mResourceResolver.clear();
+ // Handle inheritance
+ mConfiguration.syncFolderConfig();
+ updateForkStatus();
+ updateSize();
+ }
+
+ // Sanity check to make sure things are working correctly
+ if (DEBUG) {
+ RenderPreviewMode mode = mManager.getMode();
+ if (mode == DEFAULT) {
+ assert mConfiguration instanceof VaryingConfiguration;
+ VaryingConfiguration config = (VaryingConfiguration) mConfiguration;
+ int alternateFlags = config.getAlternateFlags();
+ switch (alternateFlags) {
+ case Configuration.CFG_DEVICE_STATE: {
+ State configState = config.getDeviceState();
+ State chooserState = mManager.getChooser().getConfiguration()
+ .getDeviceState();
+ assert configState != null && chooserState != null;
+ assert !configState.getName().equals(chooserState.getName())
+ : configState.toString() + ':' + chooserState;
+
+ Device configDevice = config.getDevice();
+ Device chooserDevice = mManager.getChooser().getConfiguration()
+ .getDevice();
+ assert configDevice != null && chooserDevice != null;
+ assert configDevice == chooserDevice
+ : configDevice.toString() + ':' + chooserDevice;
+
+ break;
+ }
+ case Configuration.CFG_DEVICE: {
+ Device configDevice = config.getDevice();
+ Device chooserDevice = mManager.getChooser().getConfiguration()
+ .getDevice();
+ assert configDevice != null && chooserDevice != null;
+ assert configDevice != chooserDevice
+ : configDevice.toString() + ':' + chooserDevice;
+
+ State configState = config.getDeviceState();
+ State chooserState = mManager.getChooser().getConfiguration()
+ .getDeviceState();
+ assert configState != null && chooserState != null;
+ assert configState.getName().equals(chooserState.getName())
+ : configState.toString() + ':' + chooserState;
+
+ break;
+ }
+ case Configuration.CFG_LOCALE: {
+ Locale configLocale = config.getLocale();
+ Locale chooserLocale = mManager.getChooser().getConfiguration()
+ .getLocale();
+ assert configLocale != null && chooserLocale != null;
+ assert configLocale != chooserLocale
+ : configLocale.toString() + ':' + chooserLocale;
+ break;
+ }
+ default: {
+ // Some other type of override I didn't anticipate
+ assert false : alternateFlags;
+ }
+ }
+ }
+ }
+
+ mDirty = 0;
+ mManager.scheduleRender(this);
+ }
+
+ private void updateSize() {
+ Device device = mConfiguration.getDevice();
+ if (device == null) {
+ return;
+ }
+ Screen screen = device.getDefaultHardware().getScreen();
+ if (screen == null) {
+ return;
+ }
+
+ FolderConfiguration folderConfig = mConfiguration.getFullConfig();
+ ScreenOrientationQualifier qualifier = folderConfig.getScreenOrientationQualifier();
+ ScreenOrientation orientation = qualifier == null
+ ? ScreenOrientation.PORTRAIT : qualifier.getValue();
+
+ // compute width and height to take orientation into account.
+ int x = screen.getXDimension();
+ int y = screen.getYDimension();
+ int screenWidth, screenHeight;
+
+ if (x > y) {
+ if (orientation == ScreenOrientation.LANDSCAPE) {
+ screenWidth = x;
+ screenHeight = y;
+ } else {
+ screenWidth = y;
+ screenHeight = x;
+ }
+ } else {
+ if (orientation == ScreenOrientation.LANDSCAPE) {
+ screenWidth = y;
+ screenHeight = x;
+ } else {
+ screenWidth = x;
+ screenHeight = y;
+ }
+ }
+
+ int width = RenderPreviewManager.getMaxWidth();
+ int height = RenderPreviewManager.getMaxHeight();
+ if (screenWidth > 0) {
+ double scale = getScale(screenWidth, screenHeight);
+ width = (int) (screenWidth * scale);
+ height = (int) (screenHeight * scale);
+ }
+
+ if (width != mWidth || height != mHeight) {
+ mWidth = width;
+ mHeight = height;
+
+ Image thumbnail = mThumbnail;
+ mThumbnail = null;
+ if (thumbnail != null) {
+ thumbnail.dispose();
+ }
+ if (mHeight != 0) {
+ mAspectRatio = mWidth / (double) mHeight;
+ }
+ }
+ }
+
+ /**
+ * Returns the configuration associated with this preview
+ *
+ * @return the configuration
+ */
+ @NonNull
+ public Configuration getConfiguration() {
+ return mConfiguration;
+ }
+
+ // ---- Implements IJobChangeListener ----
+
+ @Override
+ public void aboutToRun(IJobChangeEvent event) {
+ }
+
+ @Override
+ public void awake(IJobChangeEvent event) {
+ }
+
+ @Override
+ public void done(IJobChangeEvent event) {
+ mJob = null;
+ }
+
+ @Override
+ public void running(IJobChangeEvent event) {
+ }
+
+ @Override
+ public void scheduled(IJobChangeEvent event) {
+ }
+
+ @Override
+ public void sleeping(IJobChangeEvent event) {
+ }
+
+ // ---- Delayed Rendering ----
+
+ private final class RenderJob extends UIJob {
+ public RenderJob() {
+ super("RenderPreview");
+ setSystem(true);
+ setUser(false);
+ }
+
+ @Override
+ public IStatus runInUIThread(IProgressMonitor monitor) {
+ mJob = null;
+ if (!mCanvas.isDisposed()) {
+ renderSync();
+ mCanvas.redraw();
+ return org.eclipse.core.runtime.Status.OK_STATUS;
+ }
+
+ return org.eclipse.core.runtime.Status.CANCEL_STATUS;
+ }
+
+ @Override
+ public Display getDisplay() {
+ if (mCanvas.isDisposed()) {
+ return null;
+ }
+ return mCanvas.getDisplay();
+ }
+ }
+
+ private final class AsyncRenderJob extends Job {
+ public AsyncRenderJob() {
+ super("RenderPreview");
+ setSystem(true);
+ setUser(false);
+ }
+
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ mJob = null;
+
+ if (mCanvas.isDisposed()) {
+ return org.eclipse.core.runtime.Status.CANCEL_STATUS;
+ }
+
+ renderSync();
+
+ // Update display
+ mCanvas.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ mCanvas.redraw();
+ }
+ });
+
+ return org.eclipse.core.runtime.Status.OK_STATUS;
+ }
+ }
+
+ /**
+ * Sets the input file to use for rendering. If not set, this will just be
+ * the same file as the configuration chooser. This is used to render other
+ * layouts, such as variations of the currently edited layout, which are
+ * not kept in sync with the main layout.
+ *
+ * @param file the file to set as input
+ */
+ public void setAlternateInput(@Nullable IFile file) {
+ mAlternateInput = file;
+ }
+
+ /** Corresponding description for this preview if it is a manually added preview */
+ private @Nullable ConfigurationDescription mDescription;
+
+ /**
+ * Sets the description of this preview, if this preview is a manually added preview
+ *
+ * @param description the description of this preview
+ */
+ public void setDescription(@Nullable ConfigurationDescription description) {
+ mDescription = description;
+ }
+
+ /**
+ * Returns the description of this preview, if this preview is a manually added preview
+ *
+ * @return the description
+ */
+ @Nullable
+ public ConfigurationDescription getDescription() {
+ return mDescription;
+ }
+
+ @Override
+ public String toString() {
+ return getDisplayName() + ':' + mConfiguration;
+ }
+
+ /** Sorts render previews into increasing aspect ratio order */
+ static Comparator<RenderPreview> INCREASING_ASPECT_RATIO = new Comparator<RenderPreview>() {
+ @Override
+ public int compare(RenderPreview preview1, RenderPreview preview2) {
+ return (int) Math.signum(preview1.mAspectRatio - preview2.mAspectRatio);
+ }
+ };
+ /** Sorts render previews into visual order: row by row, column by column */
+ static Comparator<RenderPreview> VISUAL_ORDER = new Comparator<RenderPreview>() {
+ @Override
+ public int compare(RenderPreview preview1, RenderPreview preview2) {
+ int delta = preview1.mY - preview2.mY;
+ if (delta == 0) {
+ delta = preview1.mX - preview2.mX;
+ }
+ return delta;
+ }
+ };
+}