diff options
author | Bob Badour <bbadour@google.com> | 2020-05-06 14:17:12 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2020-05-06 14:17:12 +0000 |
commit | fc7cda06f54946e3a03ea008c1ba086d90aeef84 (patch) | |
tree | fd845444b59dfc72656b7781596e0b1a0662c4c7 /eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java | |
parent | 1aa104fb55f1b87494e4e2bc38e23416f486adfd (diff) | |
parent | 512ba8f80c0a2422f9e58a1401cb142db190a56c (diff) | |
download | sdk-fc7cda06f54946e3a03ea008c1ba086d90aeef84.tar.gz |
Merge "Revert "Remove unused project.""
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.java | 1333 |
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; + } + }; +} |