com.intellij.android.designer.designSurface.AndroidDesignerEditorPanel.java Source code

Java tutorial

Introduction

Here is the source code for com.intellij.android.designer.designSurface.AndroidDesignerEditorPanel.java

Source

/*
 * Copyright 2000-2012 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.android.designer.designSurface;

import com.android.ide.common.rendering.api.RenderSession;
import com.android.ide.common.rendering.api.ViewInfo;
import com.android.ide.common.sdk.SdkVersionInfo;
import com.android.resources.Density;
import com.android.sdklib.IAndroidTarget;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.configurations.ConfigurationListener;
import com.android.tools.idea.configurations.ConfigurationToolBar;
import com.android.tools.idea.configurations.RenderContext;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.gradle.variant.view.BuildVariantView;
import com.android.tools.idea.rendering.*;
import com.android.tools.idea.rendering.multi.RenderPreviewManager;
import com.android.tools.idea.rendering.multi.RenderPreviewMode;
import com.google.common.primitives.Ints;
import com.intellij.android.designer.componentTree.AndroidTreeDecorator;
import com.intellij.android.designer.inspection.ErrorAnalyzer;
import com.intellij.android.designer.model.*;
import com.intellij.android.designer.model.layout.actions.ToggleRenderModeAction;
import com.intellij.designer.DesignerEditor;
import com.intellij.designer.DesignerToolWindowManager;
import com.intellij.designer.actions.DesignerActionPanel;
import com.intellij.designer.componentTree.TreeComponentDecorator;
import com.intellij.designer.designSurface.*;
import com.intellij.designer.designSurface.tools.ComponentCreationFactory;
import com.intellij.designer.designSurface.tools.ComponentPasteFactory;
import com.intellij.designer.designSurface.tools.InputTool;
import com.intellij.designer.model.RadComponent;
import com.intellij.designer.model.RadVisualComponent;
import com.intellij.designer.model.WrapInProvider;
import com.intellij.designer.palette.DefaultPaletteItem;
import com.intellij.designer.palette.PaletteGroup;
import com.intellij.designer.palette.PaletteItem;
import com.intellij.designer.palette.PaletteToolWindowManager;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.ReadonlyStatusHandler;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.xml.XmlFile;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.util.Alarm;
import com.intellij.util.PsiNavigateUtil;
import com.intellij.util.ThrowableConsumer;
import com.intellij.util.ThrowableRunnable;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.refactoring.AndroidExtractAsIncludeAction;
import org.jetbrains.android.refactoring.AndroidExtractStyleAction;
import org.jetbrains.android.refactoring.AndroidInlineIncludeAction;
import org.jetbrains.android.refactoring.AndroidInlineStyleReferenceAction;
import org.jetbrains.android.sdk.AndroidPlatform;
import org.jetbrains.android.uipreview.AndroidLayoutPreviewPanel;
import org.jetbrains.android.uipreview.RenderingException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static com.android.tools.idea.configurations.ConfigurationListener.MASK_ALL;
import static com.android.tools.idea.configurations.ConfigurationListener.MASK_RENDERING;
import static com.android.tools.idea.gradle.variant.view.BuildVariantView.BuildVariantSelectionChangeListener;
import static com.android.tools.idea.rendering.RenderErrorPanel.SIZE_ERROR_PANEL_DYNAMICALLY;
import static com.intellij.designer.designSurface.ZoomType.FIT_INTO;

/**
 * @author Alexander Lobas
 */
public final class AndroidDesignerEditorPanel extends DesignerEditorPanel
        implements RenderContext, BuildVariantSelectionChangeListener {
    private static final int DEFAULT_HORIZONTAL_MARGIN = 30;
    private static final int DEFAULT_VERTICAL_MARGIN = 20;
    private static final Integer LAYER_ERRORS = LAYER_INPLACE_EDITING + 150; // Must be an Integer, not an int; see JLayeredPane.addImpl
    private static final Integer LAYER_PREVIEW = LAYER_INPLACE_EDITING + 170; // Must be an Integer, not an int; see JLayeredPane.addImpl

    private final TreeComponentDecorator myTreeDecorator = new AndroidTreeDecorator();
    private final XmlFile myXmlFile;
    private final ExternalPSIChangeListener myPsiChangeListener;
    private final Alarm mySessionAlarm = new Alarm();
    private final MergingUpdateQueue mySessionQueue;
    private final AndroidDesignerEditorPanel.LayoutConfigurationListener myConfigListener;
    private final AndroidFacet myFacet;
    private volatile RenderSession mySession;
    private volatile long mySessionId;
    private final Lock myRendererLock = new ReentrantLock();
    private WrapInProvider myWrapInProvider;
    private RootView myRootView;
    private boolean myShowingRoot;
    private RenderPreviewTool myPreviewTool;

    @Nullable
    private Configuration myConfiguration;
    private int myConfigurationDirty;
    private boolean myActive;
    private boolean myVariantChanged;

    /** Zoom level (1 = 100%). TODO: Persist this setting across IDE sessions (on a per file basis) */
    private double myZoom = 1;
    private ZoomType myZoomMode = ZoomType.FIT_INTO;
    private RenderPreviewManager myPreviewManager;

    public AndroidDesignerEditorPanel(@NotNull DesignerEditor editor, @NotNull Project project,
            @NotNull Module module, @NotNull VirtualFile file) {
        super(editor, project, module, file);

        showProgress("Loading configuration...");

        AndroidFacet facet = AndroidFacet.getInstance(getModule());
        assert facet != null;
        myFacet = facet;
        // The configuration depends on project state, which may not yet be available: defer
        boolean initializeConfiguration = true;
        if (facet.isGradleProject()) {
            IdeaAndroidProject gradleProject = facet.getIdeaAndroidProject();
            if (gradleProject == null) {
                initializeConfiguration = false;
                // Still syncing model; typically on IDE restart when the editor is reopened but
                // project model has not yet been fully initialized
                facet.addListener(new AndroidFacet.GradleProjectAvailableListener() {
                    @Override
                    public void gradleProjectAvailable(@NotNull IdeaAndroidProject project) {
                        myFacet.removeListener(this);
                        initializeConfiguration();
                        requestRender();
                    }
                });
            }
            BuildVariantView variantView = BuildVariantView.getInstance(facet.getModule().getProject());
            if (variantView != null) {
                // Ensure that the project resources have been initialized first, since
                // we want it to add its own variant listeners before ours (such that
                // when the variant changes, the project resources get notified and updated
                // before our own update listener attempts a re-render)
                facet.getProjectResources(false /*libraries*/, true /*createIfNecessary*/);

                variantView.removeListener(this);
                variantView.addListener(this);
            }
        }
        myConfigListener = new LayoutConfigurationListener();
        if (initializeConfiguration) {
            initializeConfiguration();
        }

        mySessionQueue = ViewsMetaManager.getInstance(project).getSessionQueue();
        myXmlFile = (XmlFile) ApplicationManager.getApplication().runReadAction(new Computable<PsiFile>() {
            @Override
            @Nullable
            public PsiFile compute() {
                return PsiManager.getInstance(getProject()).findFile(myFile);
            }
        });
        myPsiChangeListener = new ExternalPSIChangeListener(this, myXmlFile, 100, new Runnable() {
            @Override
            public void run() {
                reparseFile();
            }
        });

        addActions();

        myActive = true;
        myPsiChangeListener.setInitialize();
        myPsiChangeListener.activate();
        myPsiChangeListener.addRequest();
    }

    private void initializeConfiguration() {
        myConfiguration = myFacet.getConfigurationManager().getConfiguration(myFile);
        myConfiguration.addListener(myConfigListener);
    }

    private void addActions() {
        addConfigurationActions();
        myActionPanel.getPopupGroup().addSeparator();
        myActionPanel.getPopupGroup().add(buildRefactorActionGroup());
        addGotoDeclarationAction();
    }

    private void addGotoDeclarationAction() {
        AnAction gotoDeclaration = new AnAction("Go To Declaration") {
            @Override
            public void update(AnActionEvent e) {
                EditableArea area = e.getData(EditableArea.DATA_KEY);
                e.getPresentation().setEnabled(area != null && area.getSelection().size() == 1);
            }

            @Override
            public void actionPerformed(AnActionEvent e) {
                EditableArea area = e.getData(EditableArea.DATA_KEY);
                if (area != null) {
                    RadViewComponent component = (RadViewComponent) area.getSelection().get(0);
                    PsiNavigateUtil.navigate(component.getTag());
                }
            }
        };
        myActionPanel.registerAction(gotoDeclaration, IdeActions.ACTION_GOTO_DECLARATION);
        myActionPanel.getPopupGroup().add(gotoDeclaration);
    }

    private void addConfigurationActions() {
        DefaultActionGroup designerActionGroup = getActionPanel().getActionGroup();
        ActionGroup group = ConfigurationToolBar.createActions(this);
        designerActionGroup.add(group);
    }

    @Override
    protected DesignerActionPanel createActionPanel() {
        return new AndroidDesignerActionPanel(this, myGlassLayer);
    }

    @Override
    protected CaptionPanel createCaptionPanel(boolean horizontal) {
        // No borders; not necessary since we have a different designer background than the caption area
        return new CaptionPanel(this, horizontal, false);
    }

    @Override
    protected JScrollPane createScrollPane(@NotNull JLayeredPane content) {
        // No background color
        JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(content);
        scrollPane.setBackground(null);
        return scrollPane;
    }

    @NotNull
    private static ActionGroup buildRefactorActionGroup() {
        final DefaultActionGroup group = new DefaultActionGroup("_Refactor", true);
        final ActionManager manager = ActionManager.getInstance();

        AnAction action = manager.getAction(AndroidExtractStyleAction.ACTION_ID);
        group.add(new AndroidRefactoringActionWrapper("_Extract Style...", action));

        action = manager.getAction(AndroidInlineStyleReferenceAction.ACTION_ID);
        group.add(new AndroidRefactoringActionWrapper("_Inline Style...", action));

        action = manager.getAction(AndroidExtractAsIncludeAction.ACTION_ID);
        group.add(new AndroidRefactoringActionWrapper("E_xtract Layout...", action));

        action = manager.getAction(AndroidInlineIncludeAction.ACTION_ID);
        group.add(new AndroidRefactoringActionWrapper("I_nline Layout...", action));
        return group;
    }

    private void reparseFile() {
        try {
            storeState();
            showDesignerCard();

            parseFile(new Runnable() {
                @Override
                public void run() {
                    showDesignerCard();
                    myLayeredPane.revalidate();
                    restoreState();
                }
            });
        } catch (RuntimeException e) {
            myPsiChangeListener.clear();
            showError("Parsing error", e.getCause() == null ? e : e.getCause());
        }
    }

    private void parseFile(final Runnable runnable) {
        final ModelParser parser = new ModelParser(getProject(), myXmlFile);
        if (myConfiguration == null) {
            return;
        }

        createRenderer(new MyThrowable(), new ThrowableConsumer<RenderResult, Throwable>() {
            @Override
            public void consume(RenderResult result) throws Throwable {
                RenderSession session = result.getSession();
                if (session == null) {
                    return;
                }

                if (!session.getResult().isSuccess()) {
                    // This image may not have been fully rendered before some error caused
                    // the render to abort, but a partial render is better. However, if the render
                    // was due to some configuration change, we don't want to replace the image
                    // since all the mouse regions and model setup will no longer match the pixels.
                    if (myRootView != null && myRootView.getImage() != null && session.getImage() != null
                            && session.getImage().getWidth() == myRootView.getImage().getWidth()
                            && session.getImage().getHeight() == myRootView.getImage().getHeight()) {
                        myRootView.setImage(session.getImage(), session.isAlphaChannelImage());
                        myRootView.repaint();
                    }
                    return;
                }

                boolean insertPanel = !myShowingRoot;
                if (myRootView == null) {
                    myRootView = new RootView(AndroidDesignerEditorPanel.this, 0, 0, session.getImage(),
                            session.isAlphaChannelImage());
                    myRootView.addComponentListener(new ComponentAdapter() {
                        @Override
                        public void componentResized(ComponentEvent componentEvent) {
                            zoomToFitIfNecessary();
                        }
                    });
                    insertPanel = true;
                } else {
                    myRootView.setImage(session.getImage(), session.isAlphaChannelImage());
                    myRootView.updateBounds(true);
                }
                try {
                    parser.updateRootComponent(myConfiguration.getFullConfig(), session, myRootView);
                } catch (Throwable e) {
                    myRootComponent = parser.getRootComponent();
                    throw e;
                }
                RadViewComponent newRootComponent = parser.getRootComponent();

                newRootComponent.setClientProperty(ModelParser.XML_FILE_KEY, myXmlFile);
                newRootComponent.setClientProperty(ModelParser.MODULE_KEY, AndroidDesignerEditorPanel.this);
                newRootComponent.setClientProperty(TreeComponentDecorator.KEY, myTreeDecorator);

                IAndroidTarget target = myConfiguration.getTarget();
                assert target != null; // otherwise, rendering would not have succeeded
                PropertyParser propertyParser = new PropertyParser(getModule(), target);
                newRootComponent.setClientProperty(PropertyParser.KEY, propertyParser);
                propertyParser.loadRecursive(newRootComponent);

                boolean firstRender = myRootComponent == null;

                myRootComponent = newRootComponent;

                // Start out selecting the root layout rather than the device item; this will
                // show relevant layout actions immediately, will cause the component tree to
                // be properly expanded, etc
                if (firstRender) {
                    RadViewComponent rootComponent = getLayoutRoot();
                    if (rootComponent != null) {
                        mySurfaceArea.setSelection(Collections.<RadComponent>singletonList(rootComponent));
                    }
                }

                if (insertPanel) {
                    // Use a custom layout manager which adjusts the margins/padding around the designer canvas
                    // dynamically; it will try to use DEFAULT_HORIZONTAL_MARGIN * DEFAULT_VERTICAL_MARGIN, but
                    // if there is not enough room, it will split the margins evenly in each dimension until
                    // there is no room available without scrollbars.
                    JPanel rootPanel = new JPanel(new LayoutManager() {
                        @Override
                        public void addLayoutComponent(String s, Component component) {
                        }

                        @Override
                        public void removeLayoutComponent(Component component) {
                        }

                        @Override
                        public Dimension preferredLayoutSize(Container container) {
                            return new Dimension(0, 0);
                        }

                        @Override
                        public Dimension minimumLayoutSize(Container container) {
                            return new Dimension(0, 0);
                        }

                        @Override
                        public void layoutContainer(Container container) {
                            myRootView.updateBounds(false);
                            int x = Math.max(2, Math.min(DEFAULT_HORIZONTAL_MARGIN,
                                    (container.getWidth() - myRootView.getWidth()) / 2));
                            int y = Math.max(2, Math.min(DEFAULT_VERTICAL_MARGIN,
                                    (container.getHeight() - myRootView.getHeight()) / 2));

                            // If we're squeezing the image to fit, and there's a drop shadow showing
                            // shift *some* space away from the tail portion of the drop shadow over to
                            // the left to make the image look more balanced
                            if (myRootView.getShowDropShadow()) {
                                if (x <= 2) {
                                    x += ShadowPainter.SHADOW_SIZE / 3;
                                }
                                if (y <= 2) {
                                    y += ShadowPainter.SHADOW_SIZE / 3;
                                }
                            }

                            if (myMaxWidth > 0) {
                                myRootView.setLocation(Math.max(0, (myMaxWidth - myRootView.getScaledWidth()) / 2),
                                        2 + Math.max(0, (myMaxHeight - myRootView.getScaledHeight()) / 2));
                            }
                        }
                    });

                    rootPanel.setBackground(AndroidLayoutPreviewPanel.DESIGNER_BACKGROUND_COLOR);
                    rootPanel.setOpaque(true);
                    rootPanel.add(myRootView);
                    myLayeredPane.add(rootPanel, LAYER_COMPONENT);
                    myShowingRoot = true;
                }
                zoomToFitIfNecessary();

                loadInspections(new EmptyProgressIndicator());
                updateInspections();

                if (RenderPreviewMode.getCurrent() != RenderPreviewMode.NONE) {
                    RenderPreviewManager previewManager = getPreviewManager(true);
                    if (previewManager != null) {
                        previewManager.renderPreviews();
                    }
                }

                runnable.run();
            }
        });
    }

    private void createRenderer(final MyThrowable throwable,
            final ThrowableConsumer<RenderResult, Throwable> runnable) {
        disposeRenderer();
        if (myConfiguration == null) {
            return;
        }

        mySessionAlarm.addRequest(new Runnable() {
            @Override
            public void run() {
                if (mySession == null) {
                    showProgress("Initializing Rendering Library...");
                }
            }
        }, 500);

        final long sessionId = ++mySessionId;

        mySessionQueue.queue(new Update("render") {
            private void cancel() {
                mySessionAlarm.cancelAllRequests();
                ApplicationManager.getApplication().invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        if (!isProjectClosed()) {
                            hideProgress();
                        }
                    }
                });
            }

            @Override
            public void run() {
                try {
                    if (sessionId != mySessionId) {
                        cancel();
                        return;
                    }

                    final Module module = getModule();
                    final RenderLogger logger = new RenderLogger(myFile.getName(), module);

                    AndroidFacet facet = AndroidFacet.getInstance(module);
                    if (facet == null) {
                        logger.error(null, "No Android facet available", null);
                    } else if (myConfiguration.getTarget() == null) {
                        logger.error(null, "No render target selected", null);
                    } else if (myConfiguration.getTheme() == null) {
                        logger.error(null, "No theme selected", null);
                    }

                    if (logger.hasProblems()) {
                        cancel();
                        RenderResult renderResult = new RenderResult(null, null, myXmlFile, logger);
                        runnable.consume(renderResult);
                        updateErrors(renderResult);
                        return;
                    }

                    final RenderResult renderResult;
                    RenderContext renderContext = AndroidDesignerEditorPanel.this;
                    if (myRendererLock.tryLock()) {
                        try {
                            final RenderService service = RenderService.create(facet, module, myXmlFile,
                                    myConfiguration, logger, renderContext);
                            if (service != null) {
                                // Prefetch outside of read lock
                                service.getResourceResolver();
                                renderResult = ApplicationManager.getApplication()
                                        .runReadAction(new Computable<RenderResult>() {
                                            @Nullable
                                            @Override
                                            public RenderResult compute() {
                                                if (!ToggleRenderModeAction.isRenderViewPort()) {
                                                    service.useDesignMode(myXmlFile.getRootTag());
                                                }
                                                return service.render();
                                            }
                                        });
                                service.dispose();
                            } else {
                                renderResult = new RenderResult(null, null, myXmlFile, logger);
                            }
                        } finally {
                            myRendererLock.unlock();
                        }
                    } else {
                        cancel();
                        return;
                    }

                    if (sessionId != mySessionId) {
                        cancel();
                        return;
                    }

                    if (renderResult == null) {
                        throw new RenderingException();
                    }

                    mySessionAlarm.cancelAllRequests();

                    ApplicationManager.getApplication().invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                if (!isProjectClosed()) {
                                    hideProgress();
                                    if (sessionId == mySessionId) {
                                        runnable.consume(renderResult);
                                        updateErrors(renderResult);
                                    }
                                }
                            } catch (Throwable e) {
                                myPsiChangeListener.clear();
                                showError("Parsing error", throwable.wrap(e));
                            }
                        }
                    });
                } catch (final Throwable e) {
                    myPsiChangeListener.clear();
                    mySessionAlarm.cancelAllRequests();

                    ApplicationManager.getApplication().invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            myPsiChangeListener.clear();
                            showError("Render error", throwable.wrap(e));
                        }
                    });
                }
            }
        });
    }

    public int getDpi() {
        return myConfiguration != null ? myConfiguration.getDensity().getDpiValue() : Density.DEFAULT_DENSITY;
    }

    private MyRenderPanelWrapper myErrorPanelWrapper;

    private void updateErrors(@NotNull RenderResult result) {
        RenderLogger logger = result.getLogger();
        if (!logger.hasProblems()) {
            if (myErrorPanelWrapper == null) {
                return;
            }
            myLayeredPane.remove(myErrorPanelWrapper);
            myErrorPanelWrapper = null;
            myLayeredPane.repaint();
        } else {
            if (myErrorPanelWrapper == null) {
                myErrorPanelWrapper = new MyRenderPanelWrapper(new RenderErrorPanel());
            }
            myErrorPanelWrapper.getErrorPanel().showErrors(result);
            myLayeredPane.add(myErrorPanelWrapper, LAYER_ERRORS);
            myLayeredPane.repaint();
        }
    }

    private void disposeRenderer() {
        if (mySession != null) {
            mySession.dispose();
            mySession = null;
        }
    }

    private void updateRenderer(final boolean updateProperties) {
        if (myConfiguration == null) {
            return;
        }
        if (myRootComponent == null) {
            reparseFile();
            return;
        }
        createRenderer(new MyThrowable(), new ThrowableConsumer<RenderResult, Throwable>() {
            @Override
            public void consume(RenderResult result) throws Throwable {
                RenderSession session = result.getSession();
                if (session == null || session.getImage() == null) {
                    return;
                }
                RadViewComponent rootComponent = (RadViewComponent) myRootComponent;
                RootView rootView = (RootView) rootComponent.getNativeComponent();
                rootView.setImage(session.getImage(), session.isAlphaChannelImage());
                ModelParser.updateRootComponent(myConfiguration.getFullConfig(), rootComponent, session, rootView);

                zoomToFitIfNecessary();

                myLayeredPane.revalidate();
                myHorizontalCaption.update();
                myVerticalCaption.update();

                DesignerToolWindowManager.getInstance(AndroidDesignerEditorPanel.this).refresh(updateProperties);

                if (RenderPreviewMode.getCurrent() != RenderPreviewMode.NONE) {
                    RenderPreviewManager previewManager = getPreviewManager(true);
                    if (previewManager != null) {
                        previewManager.renderPreviews();
                    }
                }
            }
        });
    }

    /**
     * Auto fits the scene, if requested. This will be the case the first time
     * the layout is opened, and after orientation or device changes.
     */
    synchronized void zoomToFitIfNecessary() {
        if (isZoomToFit()) {
            if (myZoomInProgress) {
                // Prevent nested zooming when (e.g. oscillating between two values)
                return;
            }
            try {
                myZoomInProgress = true;
                updateZoom();
            } finally {
                myZoomInProgress = false;
            }
        }
    }

    private boolean myZoomInProgress;

    private void removeNativeRoot() {
        if (myRootComponent != null) {
            Component component = ((RadVisualComponent) myRootComponent).getNativeComponent();
            if (component != null) {
                myLayeredPane.remove(component.getParent());
                myShowingRoot = false;
            }
        }
    }

    @Override
    protected void configureError(@NotNull ErrorInfo info) {
        // Error messages for the user (broken custom views, missing resources, etc) are already
        // trapped during rendering and shown in the error panel. These errors are internal errors
        // in the layout editor and should instead be redirected to the log.
        info.myShowMessage = false;
        info.myShowLog = true;

        Throwable renderCreator = null;
        if (info.myThrowable instanceof MyThrowable) {
            renderCreator = info.myThrowable;
            info.myThrowable = ((MyThrowable) info.myThrowable).original;
        }

        StringBuilder builder = new StringBuilder();

        builder.append("ActiveTool: ").append(myToolProvider.getActiveTool());
        builder.append("\nSDK: ");

        try {
            AndroidPlatform platform = AndroidPlatform.getInstance(getModule());
            if (platform != null) {
                IAndroidTarget target = platform.getTarget();
                builder.append(target.getFullName()).append(" - ").append(target.getVersion());
            }
        } catch (Throwable e) {
            builder.append("<unknown>");
        }

        if (renderCreator != null) {
            builder.append("\nCreateRendererStack:\n");
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            PrintStream printStream = new PrintStream(stream);
            try {
                renderCreator.printStackTrace(printStream);
                builder.append(stream.toString());
            } finally {
                printStream.close();
            }
        }

        if (info.myThrowable instanceof IndexOutOfBoundsException && myRootComponent != null && mySession != null) {
            builder.append("\n-------- RadTree --------\n");
            ModelParser.printTree(builder, myRootComponent, 0);
            builder.append("\n-------- ViewTree(").append(mySession.getRootViews().size()).append(") --------\n");
            for (ViewInfo viewInfo : mySession.getRootViews()) {
                ModelParser.printTree(builder, viewInfo, 0);
            }
        }

        info.myMessage = builder.toString();
    }

    @Override
    protected void showErrorPage(ErrorInfo info) {
        myPsiChangeListener.clear();
        mySessionAlarm.cancelAllRequests();
        removeNativeRoot();
        super.showErrorPage(info);
    }

    @Override
    public void activate() {
        myActive = true;
        myPsiChangeListener.activate();

        if (myVariantChanged || myPsiChangeListener.isUpdateRenderer()
                || ((myConfigurationDirty & MASK_RENDERING) != 0)) {
            myVariantChanged = false;
            updateRenderer(true);
        } else if (myRootComponent != null && myRootView != null) {
            if (RenderPreviewMode.getCurrent() != RenderPreviewMode.NONE) {
                RenderPreviewManager previewManager = getPreviewManager(true);
                if (previewManager != null) {
                    previewManager.renderPreviews();
                }
            }
        }
        myConfigurationDirty = 0;
    }

    @Override
    public void deactivate() {
        myActive = false;
        myPsiChangeListener.deactivate();
    }

    public void buildProject() {
        if (myPsiChangeListener.ensureUpdateRenderer() && myRootComponent != null) {
            updateRenderer(true);
        }
    }

    @Override
    public void dispose() {
        myPsiChangeListener.dispose();
        if (myConfiguration != null) {
            myConfiguration.removeListener(myConfigListener);
        }
        super.dispose();

        disposeRenderer();

        if (myPreviewManager != null) {
            myPreviewManager.dispose();
            myPreviewManager = null;
        }
    }

    @Override
    @Nullable
    protected Module findModule(Project project, VirtualFile file) {
        Module module = super.findModule(project, file);
        if (module == null) {
            module = ApplicationManager.getApplication().runReadAction(new Computable<Module>() {
                @Nullable
                @Override
                public Module compute() {
                    return ModuleUtilCore.findModuleForPsiElement(myXmlFile);
                }
            });
        }
        return module;
    }

    @Override
    public String getPlatformTarget() {
        return "android";
    }

    @Override
    public TreeComponentDecorator getTreeDecorator() {
        return myTreeDecorator;
    }

    @Override
    public WrapInProvider getWrapInProvider() {
        if (myWrapInProvider == null) {
            myWrapInProvider = new AndroidWrapInProvider(getProject());
        }
        return myWrapInProvider;
    }

    @Override
    protected ComponentDecorator getRootSelectionDecorator() {
        return EmptyComponentDecorator.INSTANCE;
    }

    @Override
    public List<PaletteGroup> getPaletteGroups() {
        return ViewsMetaManager.getInstance(getProject()).getPaletteGroups();
    }

    @NotNull
    @Override
    public String getVersionLabel(@Nullable String version) {
        if (StringUtil.isEmpty(version)) {
            return "";
        }

        // Android versions are recorded as API integers
        Integer api = Ints.tryParse(version);
        assert api != null : version;
        int since = api.intValue();
        if (since <= 1) {
            return "";
        }

        String name = SdkVersionInfo.getAndroidName(since);

        if (name == null) {
            name = String.format("API %1$d", since);
        }

        return name;
    }

    @Override
    public boolean isDeprecated(@Nullable String deprecatedIn) {
        if (deprecatedIn == null) {
            return false;
        }

        IAndroidTarget target = myConfiguration != null ? myConfiguration.getTarget() : null;
        if (target == null) {
            return super.isDeprecated(deprecatedIn);
        }

        if (StringUtil.isEmpty(deprecatedIn)) {
            return false;
        }

        Integer api = Ints.tryParse(deprecatedIn);
        assert api != null : deprecatedIn;
        return api.intValue() <= target.getVersion().getApiLevel();
    }

    @Override
    @NotNull
    protected ComponentCreationFactory createCreationFactory(final PaletteItem paletteItem) {
        return new ComponentCreationFactory() {
            @NotNull
            @Override
            public RadComponent create() throws Exception {
                RadViewComponent component = ModelParser.createComponent(null, paletteItem.getMetaModel());
                component.setInitialPaletteItem(paletteItem);
                if (component instanceof IConfigurableComponent) {
                    ((IConfigurableComponent) component).configure(myRootComponent);
                }
                return component;
            }
        };
    }

    @Override
    public ComponentPasteFactory createPasteFactory(String xmlComponents) {
        if (myConfiguration != null) {
            IAndroidTarget target = myConfiguration.getTarget();
            if (target != null) {
                return new AndroidPasteFactory(getModule(), target, xmlComponents);
            }
        }

        return null;
    }

    private void updatePalette(IAndroidTarget target) {
        try {
            for (PaletteGroup group : getPaletteGroups()) {
                for (PaletteItem item : group.getItems()) {
                    String version = item.getVersion();
                    if (version != null) {
                        Integer api = Ints.tryParse(version);
                        assert api != null : version;
                        DefaultPaletteItem paletteItem = (DefaultPaletteItem) item;
                        paletteItem.setEnabled(api.intValue() <= target.getVersion().getApiLevel());
                    }
                }
            }

            PaletteItem item = getActivePaletteItem();
            if (item != null && !item.isEnabled()) {
                activatePaletteItem(null);
            }

            PaletteToolWindowManager.getInstance(this).refresh();
        } catch (Throwable e) {
            // Pass
        }
    }

    @Override
    public String getEditorText() {
        return ApplicationManager.getApplication().runReadAction(new Computable<String>() {
            @Override
            public String compute() {
                return myXmlFile.getText();
            }
        });
    }

    @Override
    protected boolean execute(ThrowableRunnable<Exception> operation, boolean updateProperties) {
        if (!ReadonlyStatusHandler.ensureFilesWritable(getProject(), myFile)) {
            return false;
        }
        try {
            myPsiChangeListener.stop();
            operation.run();
            updateRenderer(updateProperties);
            return true;
        } catch (Throwable e) {
            showError("Execute command", e);
            return false;
        } finally {
            myPsiChangeListener.start();
        }
    }

    @Override
    protected void executeWithReparse(ThrowableRunnable<Exception> operation) {
        if (!ReadonlyStatusHandler.ensureFilesWritable(getProject(), myFile)) {
            return;
        }
        try {
            myPsiChangeListener.stop();
            operation.run();
            myPsiChangeListener.start();
            reparseFile();
        } catch (Throwable e) {
            showError("Execute command", e);
            myPsiChangeListener.start();
        }
    }

    @Override
    protected void execute(List<EditOperation> operations) {
        if (!ReadonlyStatusHandler.ensureFilesWritable(getProject(), myFile)) {
            return;
        }
        try {
            myPsiChangeListener.stop();
            for (EditOperation operation : operations) {
                operation.execute();
            }
            updateRenderer(true);
        } catch (Throwable e) {
            showError("Execute command", e);
        } finally {
            myPsiChangeListener.start();
        }
    }

    //////////////////////////////////////////////////////////////////////////////////////////
    //
    // Inspections
    //
    //////////////////////////////////////////////////////////////////////////////////////////

    @Override
    public void loadInspections(ProgressIndicator progress) {
        if (myRootComponent != null) {
            ErrorAnalyzer.load(getProject(), myXmlFile, myRootComponent, progress);
        }
    }

    //////////////////////////////////////////////////////////////////////////////////////////
    //
    //
    //
    //////////////////////////////////////////////////////////////////////////////////////////

    private static class MyThrowable extends Throwable {
        public Throwable original;

        public MyThrowable wrap(Throwable original) {
            this.original = original;
            return this;
        }
    }

    //////////////////////////////////////////////////////////////////////////////////////////
    //
    // Zooming
    //
    //////////////////////////////////////////////////////////////////////////////////////////

    private static final double ZOOM_FACTOR = 1.2;

    public boolean isZoomToFit() {
        return myZoomMode == ZoomType.FIT || myZoomMode == ZoomType.FIT_INTO;
    }

    @Override
    public boolean isZoomSupported() {
        return true;
    }

    /** Sets the zoom level. Note that this should be 1, not 100 (percent), for an image at its actual size */
    @Override
    public void setZoom(double zoom) {
        if (myMaxWidth > 0 && myRootComponent != null) {
            // If we have a fixed size, ignore scale factor
            assert myMaxHeight > 0;
            Rectangle bounds = myRootComponent.getBounds();
            double imageWidth = bounds.getWidth();
            double imageHeight = bounds.getHeight();
            if (imageHeight > 0) {
                zoom = Math.min(myMaxWidth / imageWidth, myMaxHeight / imageHeight);
            }
        }

        if (zoom != myZoom) {
            myZoom = zoom;
            normalizeScale();
            viewZoomed();
            mySurfaceArea.scrollToSelection();
            repaint();
        }
    }

    private void normalizeScale() {
        // Some operations are faster if the zoom is EXACTLY 1.0 rather than ALMOST 1.0.
        // (This is because there is a fast-path when image copying and the scale is 1.0;
        // in that case it does not have to do any scaling).
        //
        // If you zoom out 10 times and then back in 10 times, small rounding errors mean
        // that you end up with a scale=1.0000000000000004. In the cases, when you get close
        // to 1.0, just make the zoom an exact 1.0.
        if (Math.abs(myZoom - 1.0) < 0.01) {
            myZoom = 1.0;
        }
    }

    /** Returns the current zoom level. Note that this is 1, not 100 (percent) for an image at its actual size */
    @Override
    public double getZoom() {
        return myZoom;
    }

    private int myMaxWidth;
    private int myMaxHeight;
    private boolean myUseLargeShadows = true;

    public boolean isUseLargeShadows() {
        return myUseLargeShadows;
    }

    /** Zooms the designer view */
    @Override
    public void zoom(@NotNull ZoomType type) {
        myZoomMode = type;
        updateZoom();
    }

    private void updateZoom() {
        switch (myZoomMode) {
        case IN:
            setZoom(myZoom * ZOOM_FACTOR);
            break;
        case OUT:
            setZoom(myZoom / ZOOM_FACTOR);
            break;
        case ACTUAL:
            setZoom(1);
            break;
        case FIT_INTO:
        case FIT: {
            Dimension sceneSize = myRootComponent.getBounds().getSize();
            Dimension screenSize = getDesignerViewSize();
            if (screenSize.width > 0 && screenSize.height > 0) {
                int sceneWidth = sceneSize.width;
                int sceneHeight = sceneSize.height;
                if (sceneWidth > 0 && sceneHeight > 0) {
                    int viewWidth = screenSize.width;
                    int viewHeight = screenSize.height;

                    // Reduce the margins if necessary
                    int hDelta = viewWidth - sceneWidth;
                    int xMargin = 0;
                    if (hDelta > 2 * DEFAULT_HORIZONTAL_MARGIN) {
                        xMargin = DEFAULT_HORIZONTAL_MARGIN;
                    } else if (hDelta > 0) {
                        xMargin = hDelta / 2;
                    }

                    int vDelta = viewHeight - sceneHeight;
                    int yMargin = 0;
                    if (vDelta > 2 * DEFAULT_VERTICAL_MARGIN) {
                        yMargin = DEFAULT_VERTICAL_MARGIN;
                    } else if (vDelta > 0) {
                        yMargin = vDelta / 2;
                    }

                    double hScale = (viewWidth - 2 * xMargin) / (double) sceneWidth;
                    double vScale = (viewHeight - 2 * yMargin) / (double) sceneHeight;
                    double scale = Math.min(hScale, vScale);

                    if (myZoomMode == FIT_INTO) {
                        scale = Math.min(1.0, scale);
                    }

                    setZoom(scale);
                }
            }
            break;
        }
        case SCREEN:
        default:
            throw new UnsupportedOperationException("Not yet implemented: " + myZoomMode);
        }
    }

    @NotNull
    private Dimension getDesignerViewSize() {
        Dimension size = myScrollPane.getSize();
        size.width -= 2;
        size.height -= 2;

        RootView rootView = getRootView();
        if (rootView != null) {
            if (rootView.getShowDropShadow()) {
                size.width -= ShadowPainter.SHADOW_SIZE;
                size.height -= ShadowPainter.SHADOW_SIZE;
            }

            final int MIN_SIZE = 200;
            if (myPreviewManager != null && size.width > MIN_SIZE) {
                int previewWidth = myPreviewManager.computePreviewWidth();
                size.width = Math.max(MIN_SIZE, size.width - previewWidth);
            }
        }

        return size;
    }

    @Override
    @NotNull
    protected Dimension getSceneSize(@NotNull Component target) {
        int width = 0;
        int height = 0;

        if (myRootComponent != null) {
            Rectangle bounds = myRootComponent.getBounds(target);
            width = Math.max(width, (int) bounds.getMaxX());
            height = Math.max(height, (int) bounds.getMaxY());

            width += 1;
            height += 1;

            return new Dimension(width, height);
        }

        return super.getSceneSize(target);
    }

    @Override
    protected void viewZoomed() {
        RootView rootView = getRootView();
        if (rootView != null) {
            rootView.updateSize();
        }
        revalidate();
        super.viewZoomed();
    }

    @Nullable
    private RootView getRootView() {
        if (myRootComponent instanceof RadViewComponent) {
            Component nativeComponent = ((RadViewComponent) myRootComponent).getNativeComponent();
            if (nativeComponent instanceof RootView) {
                return (RootView) nativeComponent;
            }
        }
        return null;
    }

    @Nullable
    private RadViewComponent getLayoutRoot() {
        if (myRootComponent != null && myRootComponent.getChildren().size() == 1) {
            RadComponent component = myRootComponent.getChildren().get(0);
            if (component.isBackground() && component instanceof RadViewComponent) {
                return (RadViewComponent) component;
            }
        }

        return null;
    }

    @Nullable
    @Override
    protected RadComponent findTarget(int x, int y, @Nullable ComponentTargetFilter filter) {
        RadComponent target = super.findTarget(x, y, filter);

        // If you click/drag outside the root, select the root
        if (target == null) {
            target = getLayoutRoot();
            if (target != null && filter != null && filter.preFilter(myRootComponent)) {
                filter.resultFilter(target);
            }
        }

        return target;
    }

    /**
     * Layered pane which shows the rendered image, as well as (if applicable) an error message panel on top of the rendering
     * near the bottom
     */
    private static class MyRenderPanelWrapper extends JPanel {
        private final RenderErrorPanel myErrorPanel;
        private int myErrorPanelHeight = -1;

        public MyRenderPanelWrapper(@NotNull RenderErrorPanel errorPanel) {
            super(new BorderLayout());
            myErrorPanel = errorPanel;
            setBackground(null);
            setOpaque(false);
            add(errorPanel);
        }

        private RenderErrorPanel getErrorPanel() {
            return myErrorPanel;
        }

        @Override
        public void doLayout() {
            super.doLayout();
            positionErrorPanel();
        }

        private void positionErrorPanel() {
            int height = getHeight();
            int width = getWidth();
            int size;
            if (SIZE_ERROR_PANEL_DYNAMICALLY) {
                if (myErrorPanelHeight == -1) {
                    // Make the layout take up to 3/4ths of the height, and at least 1/4th, but
                    // anywhere in between based on what the actual text requires
                    size = height * 3 / 4;
                    int preferredHeight = myErrorPanel.getPreferredHeight(width) + 8;
                    if (preferredHeight < size) {
                        size = Math.max(preferredHeight, Math.min(height / 4, size));
                        myErrorPanelHeight = size;
                    }
                } else {
                    size = myErrorPanelHeight;
                }
            } else {
                size = height / 2;
            }

            myErrorPanel.setSize(width, size);
            myErrorPanel.setLocation(0, height - size);
        }
    }

    private void saveState() {
        if (myConfiguration != null) {
            myConfiguration.save();
        }
    }

    // ---- Implements RenderContext ----

    @Override
    @Nullable
    public Configuration getConfiguration() {
        return myConfiguration;
    }

    @Override
    public void setConfiguration(@NotNull Configuration configuration) {
        if (configuration != myConfiguration) {
            if (myConfiguration != null) {
                myConfiguration.removeListener(myConfigListener);
            }
            myConfiguration = configuration;
            myConfiguration.addListener(myConfigListener);
            myConfigListener.changed(MASK_ALL);
            // TODO: Cause immediate toolbar updates?
        }
    }

    @Override
    public void requestRender() {
        updateRenderer(false);
    }

    @Override
    @NotNull
    public UsageType getType() {
        return UsageType.LAYOUT_EDITOR;
    }

    @NotNull
    @Override
    public XmlFile getXmlFile() {
        return myXmlFile;
    }

    @Nullable
    @Override
    public VirtualFile getVirtualFile() {
        return myFile;
    }

    @Override
    public boolean hasAlphaChannel() {
        return !myRootView.getShowDropShadow();
    }

    @Override
    @NotNull
    public Component getComponent() {
        return myLayeredPane;
    }

    @Override
    public void updateLayout() {
        zoom(ZoomType.FIT);
        Component component = getComponent();
        if (component instanceof JComponent) {
            JComponent jc = (JComponent) component;
            jc.revalidate();
        } else {
            component.validate();
        }
        if (myRootView != null) {
            ((JComponent) myRootView.getParent()).revalidate();
        }
        component.repaint();
    }

    @Override
    public void setDeviceFramesEnabled(boolean on) {
        // TODO
    }

    @Nullable
    @Override
    public BufferedImage getRenderedImage() {
        return myRootView != null ? myRootView.getImage() : null;
    }

    @Override
    @NotNull
    public Dimension getFullImageSize() {
        if (myRootView != null) {
            BufferedImage image = myRootView.getImage();
            if (image != null) {
                return new Dimension(image.getWidth(), image.getHeight());
            }
        }

        return NO_SIZE;
    }

    @Override
    @NotNull
    public Dimension getScaledImageSize() {
        if (myRootView != null) {
            BufferedImage image = myRootView.getImage();
            if (image != null) {
                return new Dimension((int) (myZoom * image.getWidth()), (int) (myZoom * image.getHeight()));
            }
        }

        return NO_SIZE;
    }

    @Override
    public void setMaxSize(int width, int height) {
        myMaxWidth = width;
        myMaxHeight = height;
        myUseLargeShadows = width <= 0;
    }

    @Override
    public void zoomFit(boolean onlyZoomOut, boolean allowZoomIn) {
        zoom(allowZoomIn ? ZoomType.FIT : ZoomType.FIT_INTO);
    }

    @Override
    @NotNull
    public Rectangle getClientArea() {
        return myScrollPane.getViewport().getViewRect();
    }

    @Override
    public boolean supportsPreviews() {
        return true;
    }

    @Nullable
    @Override
    public RenderPreviewManager getPreviewManager(boolean createIfNecessary) {
        if (myPreviewManager == null && createIfNecessary) {
            myPreviewManager = new RenderPreviewManager(this);
            RenderPreviewPanel panel = new RenderPreviewPanel();
            myLayeredPane.add(panel, LAYER_PREVIEW);
            myLayeredPane.revalidate();
            myLayeredPane.repaint();
        }

        return myPreviewManager;
    }

    // ---- Implements BuildVariantSelectionChangeListener ----

    @Override
    public void buildVariantSelected(@NotNull AndroidFacet facet) {
        if (facet == myFacet) {
            if (myActive) {
                // The project resources should already have been refreshed by their own variant listener
                updateRenderer(true);
            } else {
                myVariantChanged = true;
            }
        }
    }

    private class LayoutConfigurationListener implements ConfigurationListener {
        @Override
        public boolean changed(int flags) {
            if (isProjectClosed()) {
                return true;
            }

            if (myActive) {
                updateRenderer(false);

                if ((flags & CFG_TARGET) != 0) {
                    IAndroidTarget target = myConfiguration != null ? myConfiguration.getTarget() : null;
                    if (target != null) {
                        updatePalette(target);
                    }
                }

                saveState();
            } else {
                myConfigurationDirty |= flags;
            }

            return true;
        }
    }

    private class RenderPreviewPanel extends JComponent {
        RenderPreviewPanel() {
            //super(new BorderLayout());
            setBackground(null);
            setOpaque(false);
        }

        @Override
        protected void paintComponent(Graphics g) {
            if (myPreviewManager != null) {
                myPreviewManager.paint((Graphics2D) g);
            }
        }
    }

    @Override
    protected DesignerEditableArea createEditableArea() {
        return new AndroidEditableArea();
    }

    private class AndroidEditableArea extends DesignerEditableArea {
        @Override
        public InputTool findTargetTool(int x, int y) {
            if (myPreviewManager != null && myRootView != null) {
                if (myPreviewTool == null) {
                    myPreviewTool = new RenderPreviewTool();
                }

                if (x > (myRootView.getX() + myRootView.getWidth())
                        || y > (myRootView.getY() + myRootView.getHeight())) {
                    return myPreviewTool;
                }
            }

            return super.findTargetTool(x, y);
        }
    }

    private class RenderPreviewTool extends InputTool {
        @Override
        public void mouseMove(MouseEvent event, EditableArea area) throws Exception {
            if (myPreviewManager != null) {
                myPreviewManager.moved(event);
            }
        }

        @Override
        public void mouseUp(MouseEvent event, EditableArea area) throws Exception {
            super.mouseUp(event, area);
            if (myPreviewManager != null && event.getClickCount() > 0) {
                myPreviewManager.click(event);
            }
        }

        @Override
        public void mouseEntered(MouseEvent event, EditableArea area) throws Exception {
            super.mouseEntered(event, area);
            if (myPreviewManager != null) {
                myPreviewManager.enter(event);
            }
        }

        @Override
        public void mouseExited(MouseEvent event, EditableArea area) throws Exception {
            super.mouseExited(event, area);
            if (myPreviewManager != null) {
                myPreviewManager.exit(event);
            }
        }
    }
}