/* * 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.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.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 IdManager myIdManager; 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()); myIdManager = designer.getIdManager(); 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) { return component.getRoot().getClientProperty(DESIGNER_KEY); } @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; } @Nullable public static IdManager getIdManager(@NotNull RadComponent component) { return getDesigner(component).getIdManager(); } @Nullable 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(RenderResult result, RootView nativeComponent, RadViewComponent root) { root.setNativeComponent(nativeComponent); // Stash reference for the component decorator so it can show the included context 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); myIdManager.addComponent(component); 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; } }