org.moe.designer.rendering.multi.RenderPreview.java Source code

Java tutorial

Introduction

Here is the source code for org.moe.designer.rendering.multi.RenderPreview.java

Source

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.moe.designer.rendering.multi;

import com.android.ide.common.rendering.HardwareConfigHelper;
import com.android.ide.common.rendering.api.RenderSession;
import com.android.ide.common.rendering.api.Result;
import com.android.ide.common.rendering.api.Result.Status;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.resources.Density;
import com.android.resources.ScreenOrientation;
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.moe.designer.android.AndroidFacet;
import org.moe.designer.configurations.Configuration;
import org.moe.designer.configurations.NestedConfiguration;
import org.moe.designer.configurations.RenderContext;
import org.moe.designer.ddms.screenshot.DeviceArtPainter;
import org.moe.designer.ixml.IXmlFile;
import org.moe.designer.rendering.*;
import org.moe.designer.uipreview.AndroidEditorSettings;
import org.moe.designer.utils.IOSPsiUtils;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.xml.XmlFile;
import com.intellij.util.PairFunction;
import com.intellij.util.ui.UIUtil;
import icons.AndroidIcons;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.Comparator;

import static org.moe.designer.configurations.ConfigurationListener.MASK_RENDERING;
import static org.moe.designer.rendering.ShadowPainter.SMALL_SHADOW_SIZE;
import static java.awt.RenderingHints.KEY_ANTIALIASING;
import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON;

/**
 * Represents a preview rendering of a given configuration
 */
public class RenderPreview implements Disposable {
    private static final Logger LOG = Logger.getInstance("#com.android.tools.idea.rendering.RenderPreview");

    /**
     * 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 these previews support zooming at the individual level */
    private static final boolean ZOOM_SUPPORT = false;

    /**
     * Whether to dump out rendering failures of the previews to the log
     */
    private static final boolean DUMP_RENDER_DIAGNOSTICS = true;

    /**
     * Extra error checking in debug mode
     */
    private static final boolean DEBUG = false;

    /**
     * The configuration being previewed
     */
    private @NotNull Configuration myConfiguration;

    /**
     * Configuration to use if we have an alternate input to be rendered
     */
    private @NotNull Configuration myAlternateConfiguration;

    /**
     * The associated manager
     */
    private final @NotNull RenderPreviewManager myManager;

    private final @NotNull RenderContext myRenderContext;
    private @Nullable BufferedImage myThumbnail;
    private @Nullable String myDisplayName;
    private int myX;
    private int myY;
    private int myLayoutWidth;
    private int myLayoutHeight;
    private int myTitleHeight;
    private double myScale = 1.0;
    private double myAspectRatio;
    /** Whether the preview wants a device frame (but it may still not show it if the option isc currently off) */
    private boolean myShowFrame;
    /** Whether current thumbnail actually has a device frame */
    private boolean myThumbnailHasFrame;
    private @Nullable Rectangle myViewBounds;
    private @Nullable Runnable myPendingRendering;
    private @Nullable String myId;

    /**
     * If non null, points to a separate file containing the source
     */
    private @Nullable VirtualFile myAlternateInput;

    /**
     * If included within another layout, the name of that outer layout
     */
    //  private @Nullable IncludeReference myIncludedWithin;

    /**
     * Whether the mouse is actively hovering over this preview
     */
    private boolean myActive;

    /**
     * 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 myError;

    /**
     * Whether in the current layout, this preview is visible
     */
    private boolean myVisible;

    /**
     * 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 ConfigurationListener}.
     */
    private int myDirty;

    /**
     * TODO: Figure out something more memory efficient than storing all these images. Maybe resize it down to half size initially!
     * Or maybe only if it's made setVisible
     */
    private BufferedImage myFullImage;
    private int myFullWidth;
    private int myFullHeight;

    /**
     * Creates a new {@linkplain RenderPreview}
     *
     * @param manager       the manager
     * @param renderContext canvas where preview is painted
     * @param configuration the associated configuration
     * @param showFrame     whether device frames should be shown
     */
    @SuppressWarnings("AssertWithSideEffects")
    private RenderPreview(@NotNull RenderPreviewManager manager, @NotNull RenderContext renderContext,
            @NotNull Configuration configuration, boolean showFrame) {
        myManager = manager;
        myRenderContext = renderContext;
        myConfiguration = configuration;
        myShowFrame = showFrame;

        // Should only attempt to create configurations for fully configured devices
        //noinspection AssertWithSideEffects
        assert myConfiguration.getDevice() != null;
        assert myConfiguration.getDeviceState() != null;
        assert myConfiguration.getTarget() != null;
        assert myConfiguration.getTheme() != null;
        assert myConfiguration.getFullConfig().getScreenSizeQualifier() != null : myConfiguration;

        computeInitialSize();
    }

    /**
     * Considers the device screen and orientation and computes initial values for
     * the {@link #myFullWidth}, {@link #myFullHeight}, {@link #myAspectRatio},
     * {@link #myLayoutWidth} and {@link #myLayoutHeight} fields
     */
    void computeInitialSize() {
        computeFullSize();

        if (myFullHeight > 0) {
            double scale = Math.min(1, getScale(myFullWidth, myFullHeight));
            myLayoutWidth = (int) (myFullWidth * scale);
            myLayoutHeight = (int) (myFullHeight * scale);
        } else {
            myAspectRatio = 1;
            myLayoutWidth = RenderPreviewManager.getMaxWidth();
            myLayoutHeight = RenderPreviewManager.getMaxHeight();
        }
    }

    /**
     * Considers the device screen and orientation and computes values for
     * the {@link #myFullWidth}, {@link #myFullHeight}, and {@link #myAspectRatio}.
     */
    @SuppressWarnings("SuspiciousNameCombination") // Deliberately swapping width/height orientations
    private boolean computeFullSize() {
        Device device = myConfiguration.getDevice();
        if (device == null) {
            return true;
        }
        Screen screen = device.getDefaultHardware().getScreen();
        if (screen == null) {
            return true;
        }

        State deviceState = myConfiguration.getDeviceState();
        if (deviceState == null) {
            deviceState = device.getDefaultState();
        }
        ScreenOrientation orientation = deviceState.getOrientation();
        Dimension size = device.getScreenSize(orientation);
        assert size != null;
        int screenWidth = size.width;
        int screenHeight = size.height;

        boolean changed = myFullWidth != screenWidth || myFullHeight != screenHeight;
        myFullWidth = screenWidth;
        myFullHeight = screenHeight;

        if (myShowFrame) {
            DeviceArtPainter framePainter = DeviceArtPainter.getInstance();
            double xScale = framePainter.getFrameWidthOverhead(device, orientation);
            double yScale = framePainter.getFrameHeightOverhead(device, orientation);
            myFullWidth *= xScale;
            myFullHeight *= yScale;
        }

        myAspectRatio = myFullHeight == 0 ? 1 : myFullWidth / (double) myFullHeight;
        return changed;
    }

    /** Recomputes the size */
    void updateSize() {
        boolean changed = computeFullSize();
        if (changed) {
            setMaxSize(myMaxWidth, myMaxHeight);
        }
    }

    /**
     * Sets the configuration to use for this preview
     *
     * @param configuration the new configuration
     */
    public void setConfiguration(@NotNull Configuration configuration) {
        myConfiguration = configuration;
    }

    /**
     * Gets the scale being applied to the thumbnail
     *
     * @return the scale being applied to the thumbnail
     */
    public double getScale() {
        return myScale;
    }

    /**
     * Sets the scale to apply to the thumbnail
     *
     * @param scale the factor to scale the thumbnail picture by
     */
    public void setScale(double scale) {
        if (ZOOM_SUPPORT) {
            if (scale != myScale) {
                disposeThumbnail();
                myScale = scale;
            }
        }
    }

    /**
     * Returns the aspect ratio of this render preview
     *
     * @return the aspect ratio
     */
    public double getAspectRatio() {
        return myAspectRatio;
    }

    /**
     * Returns whether the preview is actively hovered
     *
     * @return whether the mouse is hovering over the preview
     */
    public boolean isActive() {
        return myActive;
    }

    /**
     * Sets whether the preview is actively hovered
     *
     * @param active if the mouse is hovering over the preview
     */
    public void setActive(boolean active) {
        myActive = 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 myVisible;
    }

    /**
     * Returns whether this preview represents a forked layout
     *
     * @return true if this preview represents a separate file
     */
    //  public boolean isForked() {
    //    return myAlternateInput != null || myIncludedWithin != 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 VirtualFile getAlternateInput() {
    //    if (myAlternateInput != null) {
    //      return myAlternateInput;
    //    }
    //    else if (myIncludedWithin != null) {
    //      return myIncludedWithin.getFromFile();
    //    }
    //
    //    return null;
    //  }

    /**
     * Returns the area of this render preview, PRIOR to scaling
     *
     * @return the area (width times height without scaling)
     */
    int getArea() {
        return myLayoutWidth * myLayoutHeight;
    }

    /**
     * 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 != myVisible) {
            myVisible = visible;
            if (myVisible) {
                if (myDirty != 0) {
                    // Just made the render preview visible:
                    configurationChanged(myDirty); // schedules render
                } else {
                    updateForkStatus();
                    myManager.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) {
    //    myX = x;
    //    myY = y;
    //  }

    /**
     * Gets the layout X position relative to the top left corner of the preview
     * area, in control coordinates
     */
    int getX() {
        return myX;
    }

    /**
     * Gets the layout Y position relative to the top left corner of the preview
     * area, in control coordinates
     */
    int getY() {
        return myY;
    }

    /**
     * Determine whether this configuration has a better match in a different layout file
     */
    private void updateForkStatus() {
        FolderConfiguration config = myConfiguration.getFullConfig();
        //    if (myAlternateInput != null && myConfiguration.isBestMatchFor(myAlternateInput, config)) {
        //      return;
        //    }

        myAlternateInput = null;
        VirtualFile editedFile = myConfiguration.getFile();
        //    if (editedFile != null) {
        //      if (!myConfiguration.isBestMatchFor(editedFile, config)) {
        //        LocalResourceRepository resources = AppResourceRepository.getAppResources(myConfiguration.getModule(), true);
        //        if (resources != null) {
        //          VirtualFile best = resources.getMatchingFile(editedFile, ResourceType.LAYOUT, config);
        //          if (best != null) {
        //            myAlternateInput = best;
        //          }
        //          if (myAlternateInput != null) {
        //            myAlternateConfiguration = Configuration.create(myConfiguration, myAlternateInput);
        //          }
        //        }
        //      }
        //    }
    }

    /**
     * Creates a new {@linkplain RenderPreview}
     *
     * @param manager       the manager
     * @param configuration the associated configuration
     * @param showFrame     whether device frames should be shown
     * @return a new configuration
     */
    @NotNull
    public static RenderPreview create(@NotNull RenderPreviewManager manager, @NotNull Configuration configuration,
            boolean showFrame) {
        RenderContext context = manager.getRenderContext();
        return new RenderPreview(manager, context, configuration, showFrame);
    }

    /**
     * Throws away this preview: cancels any pending rendering jobs and disposes
     * of image resources etc
     */
    @Override
    public void dispose() {
        disposeThumbnail();
        if (this != myManager.getStashedPreview()) {
            myConfiguration.dispose();
        }
    }

    /**
     * Disposes the thumbnail rendering.
     */
    void disposeThumbnail() {
        myThumbnail = null;
        myFullImage = null;
    }

    /**
     * Returns the display name of this preview
     *
     * @return the name of the preview
     */
    @NotNull
    public String getDisplayName() {
        if (myDisplayName == 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 myDisplayName;
    }

    /**
     * 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(@Nullable String displayName) {
        myDisplayName = 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(@Nullable IncludeReference includedWithin) {
    //    myIncludedWithin = includedWithin;
    //  }

    /**
     * Render immediately (on the current thread)
     */
    void renderSync() {
        if (!tryRenderSync()) {
            disposeThumbnail();
        }
    }

    private boolean tryRenderSync() {
        final Module module = myRenderContext.getModule();
        if (module == null) {
            return false;
        }
        AndroidFacet facet = AndroidFacet.getInstance(module);
        if (facet == null) {
            return false;
        }

        final Configuration configuration = myAlternateInput != null && myAlternateConfiguration != null
                ? myAlternateConfiguration
                : myConfiguration;
        PsiFile psiFile;
        if (myAlternateInput != null) {
            psiFile = IOSPsiUtils.getPsiFileSafely(module.getProject(), myAlternateInput);
        } else {
            psiFile = myRenderContext.getXmlFile();
        }
        if (psiFile == null) {
            return false;
        }
        RenderLogger logger = new RenderLogger(psiFile.getName(), module);
        PreviewRenderContext renderContext = new PreviewRenderContext(myRenderContext, configuration,
                (IXmlFile) psiFile);
        final RenderService renderService = RenderService.create(facet, module, psiFile, configuration, logger,
                renderContext);
        if (renderService == null) {
            return false;
        }

        //    if (myIncludedWithin != null) {
        //      renderService.setIncludedWithin(myIncludedWithin);
        //    }

        RenderResult result = renderService.render();
        RenderSession session = result != null ? result.getSession() : null;
        if (session != null) {
            Result render = session.getResult();

            if (DUMP_RENDER_DIAGNOSTICS) {
                if (logger.hasProblems() || !session.getResult().isSuccess()) {
                    //          RenderErrorPanel panel = new RenderErrorPanel();
                    //          String html = panel.showErrors(result);
                    //          LOG.info("Found problems rendering preview " + getDisplayName() + ": " + html);
                }
            }

            if (render.isSuccess()) {
                myError = null;
            } else {
                myError = render.getErrorMessage();
                if (myError == null) {
                    myError = "<unknown error>";
                }
            }

            if (render.getStatus() == Status.ERROR_TIMEOUT) {
                // TODO: Special handling? schedule update again later
                return false;
            }

            disposeThumbnail();
            if (render.isSuccess()) {
                RenderedImage renderedImage = result.getImage();
                if (renderedImage != null) {
                    myFullImage = renderedImage.getOriginalImage();
                }
            }

            if (myError != null) {
                createErrorThumbnail();
            }
            return true;
        } else {
            myError = "Render Failed";
            disposeThumbnail();
            createErrorThumbnail();
            return false;
        }
    }

    @Nullable
    private BufferedImage getThumbnail() {
        if (myThumbnail == null && myFullImage != null) {
            createThumbnail();
        }

        return myThumbnail;
    }

    /**
     * Sets the new image of the preview and generates a thumbnail
     */
    void createThumbnail() {
        BufferedImage image = myFullImage;
        if (image == null) {
            myThumbnail = null;
            return;
        }

        Project project = myConfiguration.getModule().getProject();
        AndroidEditorSettings.GlobalState settings = AndroidEditorSettings.getInstance().getGlobalState();

        if (UIUtil.isRetina() && ImageUtils.supportsRetina() && settings.isRetina() && createRetinaThumbnail()) {
            return;
        }

        int shadowSize = 0;
        myThumbnailHasFrame = false;
        boolean showFrame = myShowFrame;

        if (showFrame && settings.isShowDeviceFrames()) {
            DeviceArtPainter framePainter = DeviceArtPainter.getInstance();
            Device device = myConfiguration.getDevice();
            boolean showEffects = settings.isShowEffects();
            State deviceState = myConfiguration.getDeviceState();
            if (device != null && deviceState != null) {
                double scale = Math.min(1, getLayoutWidth() / (double) image.getWidth());
                //double scale = getLayoutWidth() / (double)image.getWidth();
                ScreenOrientation orientation = deviceState.getOrientation();
                double frameScale = framePainter.getFrameMaxOverhead(device, orientation);
                scale /= frameScale;
                if (myViewBounds == null) {
                    myViewBounds = new Rectangle();
                }
                image = framePainter.createFrame(image, device, orientation, showEffects, scale, myViewBounds);
                myThumbnailHasFrame = true;
            } else {
                // TODO: Do drop shadow painting if frame fails?
                double scale = Math.min(1, getLayoutWidth() / (double) image.getWidth());
                image = ImageUtils.scale(image, scale, scale, 0, 0);
            }
        } else {
            boolean drawShadows = !myRenderContext.hasAlphaChannel();
            double scale = Math.min(1, getLayoutWidth() / (double) image.getWidth());
            shadowSize = drawShadows ? SMALL_SHADOW_SIZE : 0;
            if (scale < 1.0) {
                image = ImageUtils.scale(image, scale, scale, shadowSize, shadowSize);
                if (drawShadows) {
                    ShadowPainter.drawSmallRectangleShadow(image, 0, 0, image.getWidth() - shadowSize,
                            image.getHeight() - shadowSize);
                }
            }
        }

        myThumbnail = image;
        if (image != null) {
            myLayoutWidth = image.getWidth() - shadowSize;
            myLayoutHeight = image.getHeight() - shadowSize;
        }
    }

    private boolean createRetinaThumbnail() {
        BufferedImage image = myFullImage;
        if (image == null) {
            myThumbnail = null;
            return true;
        }

        myThumbnailHasFrame = false;
        boolean showFrame = myShowFrame;

        AndroidEditorSettings.GlobalState settings = AndroidEditorSettings.getInstance().getGlobalState();
        if (showFrame && settings.isShowDeviceFrames()) {
            DeviceArtPainter framePainter = DeviceArtPainter.getInstance();
            Device device = myConfiguration.getDevice();
            boolean showEffects = settings.isShowEffects();
            State deviceState = myConfiguration.getDeviceState();
            if (device != null && deviceState != null) {
                double scale = getLayoutWidth() / (double) image.getWidth();
                ScreenOrientation orientation = deviceState.getOrientation();
                double frameScale = framePainter.getFrameMaxOverhead(device, orientation);
                scale /= frameScale;
                if (myViewBounds == null) {
                    myViewBounds = new Rectangle();
                }
                image = framePainter.createFrame(image, device, orientation, showEffects, 2 * scale, myViewBounds);
                myViewBounds.x /= 2;
                myViewBounds.y /= 2;
                myViewBounds.width /= 2;
                myViewBounds.height /= 2;

                myThumbnailHasFrame = true;
            } else {
                double scale = getLayoutWidth() / (double) image.getWidth();
                image = ImageUtils.scale(image, 2 * scale, 2 * scale, 0, 0);
            }

            image = ImageUtils.convertToRetina(image);
            if (image == null) {
                return false;
            }
        } else {
            boolean drawShadows = !myRenderContext.hasAlphaChannel();
            double scale = getLayoutWidth() / (double) image.getWidth();
            if (scale < 1.0) {
                image = ImageUtils.scale(image, 2 * scale, 2 * scale);

                image = ImageUtils.convertToRetina(image);
                if (image == null) {
                    return false;
                }

                myLayoutWidth = image.getWidth();
                myLayoutHeight = image.getHeight();

                if (drawShadows) {
                    image = ShadowPainter.createSmallRectangularDropShadow(image);
                }
                myThumbnail = image;
                return true;
            }
        }

        myThumbnail = image;
        myLayoutWidth = image.getWidth();
        myLayoutHeight = image.getHeight();

        return true;
    }

    void createErrorThumbnail() {
        int width = getLayoutWidth();
        int height = getLayoutHeight();
        @SuppressWarnings("UndesirableClassUsage")
        BufferedImage image = new BufferedImage(width + SMALL_SHADOW_SIZE, height + SMALL_SHADOW_SIZE,
                BufferedImage.TYPE_INT_ARGB);

        Graphics2D g = image.createGraphics();
        //noinspection UseJBColor
        g.setColor(new Color(0xfffbfcc6));
        g.fillRect(0, 0, width, height);

        g.dispose();

        boolean drawShadows = !myRenderContext.hasAlphaChannel();
        if (drawShadows) {
            ShadowPainter.drawSmallRectangleShadow(image, 0, 0, image.getWidth() - SMALL_SHADOW_SIZE,
                    image.getHeight() - SMALL_SHADOW_SIZE);
        }

        myThumbnail = image;
    }

    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) (myLayoutWidth * myScale * RenderPreviewManager.getScale());
    }

    /**
     * Returns the height of the preview, in pixels
     *
     * @return the height in pixels
     */
    public int getHeight() {
        return (int) (myLayoutHeight * myScale * RenderPreviewManager.getScale());
    }

    /**
     * Returns the <b>desired</b> width of this preview, in pixels.
     * Whereas {@link #getWidth()} returns the current width of the preview,
     * this method returns the desired with after the next render.
     * <p>
     * For example, let's say the orientation has just changed and an update has
     * been scheduled. During this interval, the width of the preview is the
     * old, un-rotated preview's width, whereas the layout width is the new
     * width after rotation has been applied.
     *
     * @return the layout width
     */
    public int getLayoutWidth() {
        return myLayoutWidth;
    }

    /**
     * Returns the <b>desired</b> height of this preview, in pixels.
     * See {@link #getLayoutWidth()} for details on how the layout height
     * is different from {@link #getHeight()}.
     */
    public int getLayoutHeight() {
        return myLayoutHeight;
    }

    /**
     * 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 >= myTitleHeight && y < myTitleHeight + HEADER_HEIGHT) {
            int left = 0;
            left += AllIcons.Actions.CloseHovered.getIconWidth();
            if (x <= left) {
                // Delete
                myManager.deletePreview(this);
                return true;
            }
            if (ZOOM_SUPPORT) {
                left += AndroidIcons.ZoomIn.getIconWidth();
                if (x <= left) {
                    // Zoom in
                    myScale *= (1 / 0.5);
                    if (Math.abs(myScale - 1.0) < 0.0001) {
                        myScale = 1.0;
                    }

                    myManager.scheduleRender(this, 0);
                    myManager.layout(true);
                    myManager.redraw();
                    return true;
                }
                left += AndroidIcons.ZoomOut.getIconWidth();
                if (x <= left) {
                    // Zoom out
                    myScale *= (0.5 / 1);
                    if (Math.abs(myScale - 1.0) < 0.0001) {
                        myScale = 1.0;
                    }
                    myManager.scheduleRender(this, 0);

                    myManager.layout(true);
                    myManager.redraw();
                    return true;
                }
            }
            left += AllIcons.Actions.Edit.getIconWidth();
            if (x <= left) {
                // Edit. For now, just rename
                Project project = myConfiguration.getConfigurationManager().getProject();
                String newName = Messages.showInputDialog(project, "Name:", "Rename Preview", null,
                        myConfiguration.getDisplayName(), null);
                if (newName != null) {
                    myConfiguration.setDisplayName(newName);
                    myManager.redraw();
                }

                return true;
            }

            // Clicked anywhere else on header
            // Perhaps open Edit dialog here?
        }

        //    myManager.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(Graphics2D gc, int x, int y) {
        myTitleHeight = paintTitle(gc, x, y, true /*showFile*/);
        y += myTitleHeight;
        y += 2;

        Component component = myRenderContext.getComponent();
        gc.setFont(UIUtil.getToolTipFont());
        FontMetrics fontMetrics = gc.getFontMetrics();
        int fontHeight = fontMetrics.getHeight();
        int fontBaseline = fontHeight - fontMetrics.getDescent();

        int width = getWidth();
        int height = getHeight();
        BufferedImage thumbnail = getThumbnail();
        if (thumbnail != null && myError == null) {
            UIUtil.drawImage(gc, thumbnail, x, y, null);

            if (myActive) {
                // TODO: Can I figure out the actual frame bounds again?
                int x1 = x;
                int y1 = y;
                int w = myLayoutWidth;
                int h = myLayoutHeight;

                if (myThumbnailHasFrame && myViewBounds != null) {
                    x1 = myViewBounds.x + x1;
                    y1 = myViewBounds.y + y1;
                    w = myViewBounds.width;
                    h = myViewBounds.height;
                }

                //noinspection UseJBColor
                gc.setColor(new Color(181, 213, 255));
                if (HardwareConfigHelper.isRound(myConfiguration.getDevice())) {
                    Stroke prevStroke = gc.getStroke();
                    gc.setStroke(new BasicStroke(3.0f));
                    Object prevAntiAlias = gc.getRenderingHint(KEY_ANTIALIASING);
                    gc.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
                    Ellipse2D.Double ellipse = new Ellipse2D.Double(x1, y1, w, h);
                    gc.draw(ellipse);
                    gc.setStroke(prevStroke);
                    gc.setRenderingHint(KEY_ANTIALIASING, prevAntiAlias);
                } else {
                    gc.drawRect(x1 - 1, y1 - 1, w + 1, h + 1);
                    gc.drawRect(x1 - 2, y1 - 2, w + 3, h + 3);
                    gc.drawRect(x1 - 3, y1 - 3, w + 5, h + 5);
                }
            }
        } else if (myError != null && !myError.isEmpty()) {
            if (thumbnail != null) {
                UIUtil.drawImage(gc, thumbnail, x, y, null);
            } else {
                //noinspection UseJBColor
                gc.setColor(Color.DARK_GRAY);
                gc.drawRect(x, y, width, height);
            }

            Shape prevClip = gc.getClip();
            gc.setClip(x, y, width, height);
            Icon icon = AndroidIcons.RenderError;

            icon.paintIcon(component, gc, x + (width - icon.getIconWidth()) / 2,
                    y + (height - icon.getIconHeight()) / 2);
            Composite prevComposite = gc.getComposite();
            gc.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f));
            //noinspection UseJBColor
            gc.setColor(Color.WHITE);
            gc.fillRect(x, y, width, height);
            gc.setComposite(prevComposite);

            String msg = myError;
            Density density = myConfiguration.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 = fontMetrics.charWidth('x');
            int charsPerLine = (width - 10) / charWidth;
            msg = SdkUtils.wrap(msg, charsPerLine, null);
            //noinspection UseJBColor
            gc.setColor(Color.BLACK);
            gc.setFont(UIUtil.getToolTipFont());
            UIUtil.applyRenderingHints(gc);
            final UIUtil.TextPainter painter = new UIUtil.TextPainter().withShadow(true).withLineSpacing(1.4f);
            for (String line : msg.split("\n")) {
                painter.appendLine(line);
            }
            final int xf = x + 5;
            final int yf = y + HEADER_HEIGHT + fontBaseline;
            painter.draw(gc, new PairFunction<Integer, Integer, Couple<Integer>>() {
                @Override
                public Couple<Integer> fun(Integer width, Integer height) {
                    return Couple.of(xf, yf);
                }
            });
            gc.setClip(prevClip);
        } else {
            //noinspection UseJBColor
            gc.setColor(Color.DARK_GRAY);
            gc.drawRect(x, y, width, height);
            Icon icon = AndroidIcons.RefreshPreview;
            Composite prevComposite = gc.getComposite();
            gc.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.35f));
            icon.paintIcon(component, gc, x + (width - icon.getIconWidth()) / 2,
                    y + (height - icon.getIconHeight()) / 2);
            gc.setComposite(prevComposite);
        }

        if (myActive && !myShowFrame) {
            int left = x;

            Composite prevComposite = gc.getComposite();
            gc.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f));
            //noinspection UseJBColor
            gc.setColor(Color.WHITE);
            gc.fillRect(left, y, x + width - left, HEADER_HEIGHT);

            y += 2;

            // Paint icons
            AllIcons.Actions.CloseHovered.paintIcon(component, gc, left, y);
            left += AllIcons.Actions.CloseHovered.getIconWidth();

            if (ZOOM_SUPPORT) {
                AndroidIcons.ZoomIn.paintIcon(component, gc, left, y);
                left += AndroidIcons.ZoomIn.getIconWidth();

                AndroidIcons.ZoomOut.paintIcon(component, gc, left, y);
                left += AndroidIcons.ZoomOut.getIconWidth();
            }

            AllIcons.Actions.Edit.paintIcon(component, gc, left, y);
            left += AllIcons.Actions.Edit.getIconWidth();

            gc.setComposite(prevComposite);
        }
    }

    /**
     * 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(Graphics2D 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(Graphics2D gc, int x, int y, boolean showFile, String displayName) {
        int titleHeight = 0;

        //    if (showFile && myIncludedWithin != null) {
        //      if (myManager.getMode() != RenderPreviewMode.INCLUDES) {
        //        displayName = "<include>";
        //      }
        //      else {
        //        // Skip: just paint footer instead
        displayName = null;
        //      }
        //    }

        int labelTop = y + 1;
        Shape prevClip = gc.getClip();
        Rectangle clipBounds = prevClip.getBounds();
        int clipWidth = myMaxWidth > 0 ? myMaxWidth : myLayoutWidth;
        gc.setClip(x, labelTop, Math.min(clipWidth, clipBounds.x + clipBounds.width - x),
                Math.min(100, clipBounds.y + clipBounds.height - labelTop));

        // 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
        gc.setFont(UIUtil.getToolTipFont());
        FontMetrics fontMetrics = gc.getFontMetrics();
        int fontHeight = fontMetrics.getHeight();
        int fontBaseline = fontHeight - fontMetrics.getDescent();

        if (displayName != null && displayName.length() > 0) {
            // Deliberately using Color.WHITE rather than JBColor.WHITE here: the background in the preview render
            // is always gray and does not vary by theme
            //noinspection UseJBColor
            gc.setColor(Color.WHITE);
            Rectangle2D extent = fontMetrics.getStringBounds(displayName, gc);
            int labelLeft = Math.max(x, x + (myLayoutWidth - (int) extent.getWidth()) / 2);
            Icon icon = null;
            Locale locale = myConfiguration.getLocale();
            if ((locale.hasLanguage() || locale.hasRegion()) && (!(myConfiguration instanceof NestedConfiguration)
                    || ((NestedConfiguration) myConfiguration).isOverridingLocale())) {
                icon = locale.getFlagImage();
            }

            if (icon != null) {
                int flagWidth = icon.getIconWidth();
                int flagHeight = icon.getIconHeight();
                labelLeft = Math.max(x + flagWidth / 2, labelLeft);
                icon.paintIcon(myRenderContext.getComponent(), gc, labelLeft - flagWidth / 2 - 1,
                        labelTop + (fontHeight - flagHeight) / 2);
                labelLeft += flagWidth / 2 + 1;
                gc.drawString(displayName, labelLeft, labelTop - (fontHeight - flagHeight) / 2 + fontBaseline);
            } else {
                gc.drawString(displayName, labelLeft, labelTop + fontBaseline);
            }

            labelTop += (int) extent.getHeight();
            titleHeight += fontHeight;
        }

        //    if (showFile && (myAlternateInput != null || myIncludedWithin != null)) {
        //      // Draw file flag, and parent folder name
        //      VirtualFile file = myAlternateInput != null ? myAlternateInput : myIncludedWithin.getFromFile();
        //      String fileName = file.getParent().getName() + File.separator + file.getName();
        //      Rectangle2D extent = fontMetrics.getStringBounds(fileName, gc);
        //      Icon icon = AllIcons.FileTypes.Xml;
        //      int iconWidth = icon.getIconWidth();
        //      int iconHeight = icon.getIconHeight();
        //
        //      int labelLeft = Math.max(x, x + (myLayoutWidth - (int)extent.getWidth() - iconWidth - 1) / 2);
        //      icon.paintIcon(myRenderContext.getComponent(), gc, labelLeft, labelTop);
        //
        //      // Deliberately using Color.DARK_GRAY rather than JBColor.GRAY here: the background in the preview render
        //      // is always gray and does not vary by theme
        //      //noinspection UseJBColor
        //      gc.setColor(Color.DARK_GRAY);
        //      labelLeft += iconWidth + 1;
        //      labelTop -= ((int)extent.getHeight() - iconHeight) / 2;
        //      gc.drawString(fileName, labelLeft, labelTop + fontBaseline);
        //
        //      titleHeight += Math.max(titleHeight, icon.getIconHeight());
        //    }

        gc.setClip(prevClip);

        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 ConfigurationListener}
     */
    public void configurationChanged(int flags) {
        if (!myVisible) {
            myDirty |= flags;
            return;
        }
        if ((flags & MASK_RENDERING) != 0) {
            updateForkStatus();
        }

        // Sanity check to make sure things are working correctly
        //    if (DEBUG) {
        //      RenderPreviewMode mode = myManager.getMode();
        //      Configuration configuration = myRenderContext.getConfiguration();
        //      if (mode == RenderPreviewMode.DEFAULT) {
        //        assert myConfiguration instanceof VaryingConfiguration;
        //        VaryingConfiguration config = (VaryingConfiguration)myConfiguration;
        //        int alternateFlags = config.getAlternateFlags();
        //        switch (alternateFlags) {
        //          case ConfigurationListener.CFG_DEVICE_STATE: {
        //            State configState = config.getDeviceState();
        //            State chooserState = configuration.getDeviceState();
        //            assert configState != null && chooserState != null;
        //            assert !configState.getName().equals(chooserState.getName()) : configState.toString() + ':' + chooserState;
        //
        //            Device configDevice = config.getDevice();
        //            Device chooserDevice = configuration.getDevice();
        //            assert configDevice != null && chooserDevice != null;
        //            assert configDevice == chooserDevice : configDevice.toString() + ':' + chooserDevice;
        //
        //            break;
        //          }
        //          case ConfigurationListener.CFG_DEVICE: {
        //            Device configDevice = config.getDevice();
        //            Device chooserDevice = configuration.getDevice();
        //            assert configDevice != null && chooserDevice != null;
        //            assert configDevice != chooserDevice : configDevice.toString() + ':' + chooserDevice;
        //
        //            State configState = config.getDeviceState();
        //            State chooserState = configuration.getDeviceState();
        //            assert configState != null && chooserState != null;
        //            assert configState.getName().equals(chooserState.getName()) : configState.toString() + ':' + chooserState;
        //
        //            break;
        //          }
        //          case ConfigurationListener.CFG_LOCALE: {
        //            Locale configLocale = config.getLocale();
        //            Locale chooserLocale = configuration.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;
        //          }
        //        }
        //      }
        //    }

        myDirty = 0;
        myManager.scheduleRender(this);
    }

    /**
     * Returns the configuration associated with this preview
     *
     * @return the configuration
     */
    @NotNull
    public Configuration getConfiguration() {
        return myConfiguration;
    }

    /**
     * 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 VirtualFile file) {
        myAlternateInput = file;
    }

    @Override
    public String toString() {
        return getDisplayName() + ':' + myConfiguration;
    }

    /**
     * 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.myAspectRatio - preview2.myAspectRatio);
        }
    };

    /**
     * Sorts render previews into decreasing aspect ratio order
     */
    static Comparator<RenderPreview> DECREASING_ASPECT_RATIO = new Comparator<RenderPreview>() {
        @Override
        public int compare(RenderPreview preview1, RenderPreview preview2) {
            return (int) Math.signum(preview2.myAspectRatio - preview1.myAspectRatio);
        }
    };

    /**
     * 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.myY - preview2.myY;
            if (delta == 0) {
                delta = preview1.myX - preview2.myX;
            }
            return delta;
        }
    };

    private int myMaxWidth;
    private int myMaxHeight;

    public void setMaxSize(int width, int height) {
        myMaxWidth = width;
        myMaxHeight = height;

        if (width == 0 || height == 0) {
            computeInitialSize();
        } else {
            double scale = Math.min(1, Math.min(width / (double) myFullWidth,
                    (height - RenderPreviewManager.TITLE_HEIGHT) / (double) myFullHeight));
            myLayoutWidth = (int) (myFullWidth * scale);
            myLayoutHeight = (int) (myFullHeight * scale);
        }

        if (myThumbnail != null && (Math.abs(myLayoutWidth - myThumbnail.getWidth() /
        // No, only for scalable image!
        /* (ImageUtils.isRetinaImage(myThumbnail) ? 2 :*/ 1/*)*/) > 1)) {
            // Note that we null out myThumbnail, we *don't* call disposeThumbnail because we
            // want to reuse the large rendering and just scale it down again
            myThumbnail = null;
        }
    }

    @Nullable
    public String getId() {
        return myId;
    }

    public void setId(@Nullable String id) {
        myId = id;
    }

    //  public int getMaxWidth() {
    //    return myMaxWidth;
    //  }
    //
    //  public int getMaxHeight() {
    //    return myMaxHeight;
    //  }

    /** Returns the current pending rendering request, if any */
    @Nullable
    public Runnable getPendingRendering() {
        return myPendingRendering;
    }

    /** Sets or clears the current pending rendering request */
    public void setPendingRendering(@Nullable Runnable pendingRendering) {
        myPendingRendering = pendingRendering;
    }

    //  public boolean isShowFrame() {
    //    return myShowFrame;
    //  }
    //
    //  public void setShowFrame(boolean showFrame) {
    //    myShowFrame = showFrame;
    //  }
}