/* * 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.ResourceFolderType; 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(), ResourceFolderType.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 = new RenderLogger(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; } }; }