All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.intellij.android.designer.model.RadModelBuilder Maven / Gradle / Ivy

/*
 * 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 = "";

  private final MetaManager myMetaManager;
  private final PropertyParser myPropertyParser;
  private final Map myTagToComponentMap = Maps.newIdentityHashMap();
  private final Map 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 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 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;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy