blob: 2c5328776815cc691b52ff465ba003d61bc98fc4 [file] [log] [blame]
/*
* Copyright (C) 2014 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 com.intellij.android.designer.model;
import com.android.ide.common.rendering.api.MergeCookie;
import com.android.ide.common.rendering.api.RenderSession;
import com.android.ide.common.rendering.api.ViewInfo;
import com.android.tools.idea.rendering.RenderResult;
import com.google.common.collect.Maps;
import com.intellij.android.designer.AndroidDesignerEditor;
import com.intellij.android.designer.designSurface.AndroidDesignerEditorPanel;
import com.intellij.android.designer.designSurface.RootView;
import com.intellij.designer.ModuleProvider;
import com.intellij.designer.componentTree.TreeComponentDecorator;
import com.intellij.designer.model.EmptyXmlTag;
import com.intellij.designer.model.MetaManager;
import com.intellij.designer.model.MetaModel;
import com.intellij.designer.model.RadComponent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.Map;
import static com.android.SdkConstants.VIEW_MERGE;
import static com.android.SdkConstants.VIEW_TAG;
import static com.android.tools.idea.rendering.IncludeReference.ATTR_RENDER_IN;
import static com.intellij.android.designer.designSurface.RootView.EMPTY_COMPONENT_SIZE;
import static com.intellij.android.designer.designSurface.RootView.VISUAL_EMPTY_COMPONENT_SIZE;
/**
* Builder responsible for building up (and synchronizing) a hierarchy of {@link com.android.ide.common.rendering.api.ViewInfo}
* objects from layoutlib with a corresponding hierarchy of {@link com.intellij.android.designer.model.RadViewComponent}
*/
public class RadModelBuilder {
private static final String DESIGNER_KEY = "DESIGNER";
// Special tag defined in the meta model file (views-meta-model.xml) defining the root node, shown as "Device Screen"
public static final String ROOT_NODE_TAG = "<root>";
private final MetaManager myMetaManager;
private final PropertyParser myPropertyParser;
private final Map<XmlTag,RadViewComponent> myTagToComponentMap = Maps.newIdentityHashMap();
private final Map<XmlTag,RadViewComponent> myMergeComponentMap = Maps.newHashMap();
private RootView myNativeComponent;
private AndroidDesignerEditorPanel myDesigner;
public RadModelBuilder(@NotNull AndroidDesignerEditorPanel designer, @NotNull PropertyParser propertyParser) {
myDesigner = designer;
myMetaManager = ViewsMetaManager.getInstance(designer.getProject());
myPropertyParser = propertyParser;
}
@Nullable
public static RadViewComponent update(@NotNull AndroidDesignerEditorPanel designer,
@NotNull RenderResult result,
@Nullable RadViewComponent prevRoot,
@NotNull RootView nativeComponent) {
PropertyParser propertyParser = designer.getPropertyParser(result);
RadModelBuilder builder = new RadModelBuilder(designer, propertyParser);
return builder.build(prevRoot, result, nativeComponent);
}
@NotNull
public static AndroidDesignerEditorPanel getDesigner(@NotNull RadComponent component) {
AndroidDesignerEditorPanel designer = component.getRoot().getClientProperty(DESIGNER_KEY);
if (designer == null) {
// This should not normally happen, but it has shown up in a few crash logs.
// Perhaps it can happen if a component is queried after it has been detached from its root
// (though it was not clear from the crash logs how that could happen). Simply
// try to work a little harder to guess the designer from the current editor in that
// case.
if (component instanceof RadViewComponent) {
Project project = ((RadViewComponent)component).getTag().getProject();
for (FileEditor editor : FileEditorManager.getInstance(project).getSelectedEditors()) {
if (editor instanceof AndroidDesignerEditor) {
return (AndroidDesignerEditorPanel)((AndroidDesignerEditor)editor).getDesignerPanel();
}
}
}
assert false;
}
return designer;
}
@Nullable
public static ModuleProvider getModuleProvider(@NotNull RadComponent component) {
return getDesigner(component);
}
@Nullable
public static Module getModule(@NotNull RadComponent component) {
ModuleProvider provider = getModuleProvider(component);
return provider != null ? provider.getModule() : null;
}
@Nullable
public static Project getProject(@NotNull RadComponent component) {
ModuleProvider provider = getModuleProvider(component);
return provider != null ? provider.getProject() : null;
}
@NotNull
public static XmlFile getXmlFile(@NotNull RadComponent component) {
return getDesigner(component).getXmlFile();
}
@Nullable
public static TreeComponentDecorator getTreeDecorator(@NotNull RadComponent component) {
return getDesigner(component).getTreeDecorator();
}
@Nullable
public static PropertyParser getPropertyParser(@NotNull RadComponent component) {
return getDesigner(component).getPropertyParser(null);
}
@Nullable
public RadViewComponent build(@Nullable RadViewComponent prevRoot,
@NotNull RenderResult result,
@NotNull RootView nativeComponent) {
myNativeComponent = nativeComponent;
RadViewComponent root = prevRoot;
XmlTag rootTag = myDesigner.getXmlFile().getRootTag();
boolean isMerge = rootTag != null && VIEW_MERGE.equals(rootTag.getName());
if (root == null || isMerge != (root.getMetaModel() == myMetaManager.getModelByTag(VIEW_MERGE))) {
try {
root = createRoot(isMerge, rootTag);
if (root == null) {
return null;
}
} catch (Exception e) {
return null;
}
}
RenderSession session = result.getSession();
assert session != null;
updateClientProperties(result, nativeComponent, root);
initTagMap(root);
root.getChildren().clear();
updateHierarchy(root, session);
// I've removed any tags that are still in the map. I could call removeComponent on these, but I'm worried
//for (RadViewComponent removed : map.values()) {
// myIdManager.removeComponent(removed, false);
//}
updateRootBounds(root, session);
return root;
}
protected void updateClientProperties(@NotNull RenderResult result, RootView nativeComponent, RadViewComponent root) {
root.setNativeComponent(nativeComponent);
// Stash reference for the component decorator so it can show the included context
//noinspection ConstantConditions
root.setClientProperty(ATTR_RENDER_IN, result.getIncludedWithin());
}
protected void updateRootBounds(RadViewComponent root, RenderSession session) {
// Ensure bounds for the root matches actual top level children
BufferedImage image = session.getImage();
Rectangle bounds = new Rectangle(0, 0, image != null ? image.getWidth() : 0, image != null ? image.getHeight() : 0);
for (RadComponent radComponent : root.getChildren()) {
bounds = bounds.union(radComponent.getBounds());
}
root.setBounds(bounds.x, bounds.y, bounds.width, bounds.height);
}
protected void updateHierarchy(RadViewComponent root, RenderSession session) {
myNativeComponent.clearEmptyRegions();
List<ViewInfo> rootViews = session.getRootViews();
if (rootViews != null) {
for (ViewInfo info : rootViews) {
updateHierarchy(root, info, 0, 0);
}
}
}
protected void initTagMap(@NotNull RadViewComponent root) {
myTagToComponentMap.clear();
for (RadViewComponent component : RadViewComponent.getViewComponents(root.getChildren())) {
gatherTags(myTagToComponentMap, component);
}
}
@Nullable
protected RadViewComponent createRoot(boolean isMerge, @Nullable XmlTag rootTag) throws Exception {
RadViewComponent root;MetaModel rootModel = myMetaManager.getModelByTag(isMerge ? VIEW_MERGE : ROOT_NODE_TAG);
assert rootModel != null;
root = RadComponentOperations.createComponent(rootTag, rootModel);
root.setClientProperty(DESIGNER_KEY, myDesigner);
myPropertyParser.load(root);
return root;
}
private static void gatherTags(Map<XmlTag, RadViewComponent> map, RadViewComponent component) {
XmlTag tag = component.getTag();
if (tag != EmptyXmlTag.INSTANCE) {
map.put(tag, component);
}
for (RadComponent child : component.getChildren()) {
if (child instanceof RadViewComponent) {
gatherTags(map, (RadViewComponent)child);
}
}
}
@Nullable
public RadViewComponent updateHierarchy(@Nullable RadViewComponent parent,
ViewInfo view,
int parentX,
int parentY) {
Object cookie = view.getCookie();
RadViewComponent component = null;
XmlTag tag = null;
boolean isMerge = false;
if (cookie instanceof XmlTag) {
tag = (XmlTag)cookie;
} else if (cookie instanceof MergeCookie) {
isMerge = true;
cookie = ((MergeCookie)cookie).getCookie();
if (cookie instanceof XmlTag) {
tag = (XmlTag)cookie;
if (myMergeComponentMap.containsKey(tag)) {
// Just expand the bounds
int left = parentX + view.getLeft();
int top = parentY + view.getTop();
int width = view.getRight() - view.getLeft();
int height = view.getBottom() - view.getTop();
RadViewComponent radViewComponent = myMergeComponentMap.get(tag);
radViewComponent.getBounds().add(new Rectangle(left, top, width, height));
return null;
}
}
}
if (tag != null) {
boolean loadProperties;
component = myTagToComponentMap.get(tag);
if (component != null) {
if (!tag.isValid()) {
component = null;
} else {
ApplicationManager.getApplication().assertReadAccessAllowed();
MetaModel modelByTag = myMetaManager.getModelByTag(tag.getName());
if (modelByTag != null && modelByTag != component.getMetaModel()) {
component = null;
}
}
}
if (component == null) {
// TODO: Construct tag name from ViewInfo's class name so we don't have to touch the PSI data structures at all
// (so we don't need a read lock)
String tagName = tag.isValid() ? tag.getName() : VIEW_TAG;
try {
MetaModel metaModel = myMetaManager.getModelByTag(tagName);
if (metaModel == null) {
metaModel = myMetaManager.getModelByTag(VIEW_TAG);
assert metaModel != null;
}
component = RadComponentOperations.createComponent(tag, metaModel);
loadProperties = true;
}
catch (Throwable e) {
throw new RuntimeException(e);
}
} else {
component.getChildren().clear();
myTagToComponentMap.remove(tag);
loadProperties = component.getParent() != parent;
}
component.setViewInfo(view);
component.setNativeComponent(myNativeComponent);
int left = parentX + view.getLeft();
int top = parentY + view.getTop();
int width = view.getRight() - view.getLeft();
int height = view.getBottom() - view.getTop();
if (width < EMPTY_COMPONENT_SIZE && height < EMPTY_COMPONENT_SIZE) {
myNativeComponent.addEmptyRegion(left, top, VISUAL_EMPTY_COMPONENT_SIZE, VISUAL_EMPTY_COMPONENT_SIZE);
}
component.setBounds(left, top, Math.max(width, VISUAL_EMPTY_COMPONENT_SIZE), Math.max(height, VISUAL_EMPTY_COMPONENT_SIZE));
if (parent != null && parent != component) {
parent.add(component, null);
if (loadProperties) {
// Load properties on a component *after* assigning parents, since that affects
// the computation of available attributes (due to layout params)
try {
myPropertyParser.load(component);
}
catch (Throwable e) {
throw new RuntimeException(e);
}
}
if (isMerge) {
myMergeComponentMap.put(tag, component);
}
}
}
if (component != null) {
parent = component;
}
parentX += view.getLeft();
parentY += view.getTop();
for (ViewInfo child : view.getChildren()) {
updateHierarchy(parent, child, parentX, parentY);
}
return component;
}
}