aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java1178
1 files changed, 1178 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java
new file mode 100644
index 000000000..03c6c3926
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java
@@ -0,0 +1,1178 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.FQCN_SPACE;
+import static com.android.SdkConstants.FQCN_SPACE_V7;
+import static com.android.SdkConstants.GESTURE_OVERLAY_VIEW;
+import static com.android.SdkConstants.VIEW_MERGE;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.layout.GridLayoutRule;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.rendering.api.MergeCookie;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.utils.Pair;
+
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.ui.views.properties.IPropertyDescriptor;
+import org.eclipse.ui.views.properties.IPropertySheetPage;
+import org.eclipse.ui.views.properties.IPropertySource;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Maps a {@link ViewInfo} in a structure more adapted to our needs.
+ * The only large difference is that we keep both the original bounds of the view info
+ * and we pre-compute the selection bounds which are absolute to the rendered image
+ * (whereas the original bounds are relative to the parent view.)
+ * <p/>
+ * Each view also knows its parent and children.
+ * <p/>
+ * We can't alter {@link ViewInfo} as it is part of the LayoutBridge and needs to
+ * have a fixed API.
+ * <p/>
+ * The view info also implements {@link IPropertySource}, which enables a linked
+ * {@link IPropertySheetPage} to display the attributes of the selected element.
+ * This class actually delegates handling of {@link IPropertySource} to the underlying
+ * {@link UiViewElementNode}, if any.
+ */
+public class CanvasViewInfo implements IPropertySource {
+
+ /**
+ * Minimal size of the selection, in case an empty view or layout is selected.
+ */
+ public static final int SELECTION_MIN_SIZE = 6;
+
+ private final Rectangle mAbsRect;
+ private final Rectangle mSelectionRect;
+ private final String mName;
+ private final Object mViewObject;
+ private final UiViewElementNode mUiViewNode;
+ private CanvasViewInfo mParent;
+ private ViewInfo mViewInfo;
+ private final List<CanvasViewInfo> mChildren = new ArrayList<CanvasViewInfo>();
+
+ /**
+ * Is this view info an individually exploded view? This is the case for views
+ * that were specially inflated by the {@link UiElementPullParser} and assigned
+ * fixed padding because they were invisible and somebody requested visibility.
+ */
+ private boolean mExploded;
+
+ /**
+ * Node sibling. This is usually null, but it's possible for a single node in the
+ * model to have <b>multiple</b> separate views in the canvas, for example
+ * when you {@code <include>} a view that has multiple widgets inside a
+ * {@code <merge>} tag. In this case, all the views have the same node model,
+ * the include tag, and selecting the include should highlight all the separate
+ * views that are linked to this node. That's what this field is all about: it is
+ * a <b>circular</b> list of all the siblings that share the same node.
+ */
+ private List<CanvasViewInfo> mNodeSiblings;
+
+ /**
+ * Constructs a {@link CanvasViewInfo} initialized with the given initial values.
+ */
+ private CanvasViewInfo(CanvasViewInfo parent, String name,
+ Object viewObject, UiViewElementNode node, Rectangle absRect,
+ Rectangle selectionRect, ViewInfo viewInfo) {
+ mParent = parent;
+ mName = name;
+ mViewObject = viewObject;
+ mViewInfo = viewInfo;
+ mUiViewNode = node;
+ mAbsRect = absRect;
+ mSelectionRect = selectionRect;
+ }
+
+ /**
+ * Returns the original {@link ViewInfo} bounds in absolute coordinates
+ * over the whole graphic.
+ *
+ * @return the bounding box in absolute coordinates
+ */
+ @NonNull
+ public Rectangle getAbsRect() {
+ return mAbsRect;
+ }
+
+ /**
+ * Returns the absolute selection bounds of the view info as a rectangle.
+ * The selection bounds will always have a size greater or equal to
+ * {@link #SELECTION_MIN_SIZE}.
+ * The width/height is inclusive (i.e. width = right-left-1).
+ * This is in absolute "screen" coordinates (relative to the rendered bitmap).
+ *
+ * @return the absolute selection bounds
+ */
+ @NonNull
+ public Rectangle getSelectionRect() {
+ return mSelectionRect;
+ }
+
+ /**
+ * Returns the view node. Could be null, although unlikely.
+ * @return An {@link UiViewElementNode} that uniquely identifies the object in the XML model.
+ * @see ViewInfo#getCookie()
+ */
+ @Nullable
+ public UiViewElementNode getUiViewNode() {
+ return mUiViewNode;
+ }
+
+ /**
+ * Returns the parent {@link CanvasViewInfo}.
+ * It is null for the root and non-null for children.
+ *
+ * @return the parent {@link CanvasViewInfo}, which can be null
+ */
+ @Nullable
+ public CanvasViewInfo getParent() {
+ return mParent;
+ }
+
+ /**
+ * Returns the list of children of this {@link CanvasViewInfo}.
+ * The list is never null. It can be empty.
+ * By contract, this.getChildren().get(0..n-1).getParent() == this.
+ *
+ * @return the children, never null
+ */
+ @NonNull
+ public List<CanvasViewInfo> getChildren() {
+ return mChildren;
+ }
+
+ /**
+ * For nodes that have multiple views rendered from a single node, such as the
+ * children of a {@code <merge>} tag included into a separate layout, return the
+ * "primary" view, the first view that is rendered
+ */
+ @Nullable
+ private CanvasViewInfo getPrimaryNodeSibling() {
+ if (mNodeSiblings == null || mNodeSiblings.size() == 0) {
+ return null;
+ }
+
+ return mNodeSiblings.get(0);
+ }
+
+ /**
+ * Returns true if this view represents one view of many linked to a single node, and
+ * where this is the primary view. The primary view is the one that will be shown
+ * in the outline for example (since we only show nodes, not views, in the outline,
+ * and therefore don't want repetitions when a view has more than one view info.)
+ *
+ * @return true if this is the primary view among more than one linked to a single
+ * node
+ */
+ private boolean isPrimaryNodeSibling() {
+ return getPrimaryNodeSibling() == this;
+ }
+
+ /**
+ * Returns the list of node sibling of this view (which <b>will include this
+ * view</b>). For most views this is going to be null, but for views that share a
+ * single node (such as widgets inside a {@code <merge>} tag included into another
+ * layout), this will provide all the views that correspond to the node.
+ *
+ * @return a non-empty list of siblings (including this), or null
+ */
+ @Nullable
+ public List<CanvasViewInfo> getNodeSiblings() {
+ return mNodeSiblings;
+ }
+
+ /**
+ * Returns all the children of the canvas view info where each child corresponds to a
+ * unique node that the user can see and select. This is intended for use by the
+ * outline for example, where only the actual nodes are displayed, not the views
+ * themselves.
+ * <p>
+ * Most views have their own nodes, so this is generally the same as
+ * {@link #getChildren}, except in the case where you for example include a view that
+ * has multiple widgets inside a {@code <merge>} tag, where all these widgets have the
+ * same node (the {@code <merge>} tag).
+ *
+ * @return list of {@link CanvasViewInfo} objects that are children of this view,
+ * never null
+ */
+ @NonNull
+ public List<CanvasViewInfo> getUniqueChildren() {
+ boolean haveHidden = false;
+
+ for (CanvasViewInfo info : mChildren) {
+ if (info.mNodeSiblings != null) {
+ // We have secondary children; must create a new collection containing
+ // only non-secondary children
+ List<CanvasViewInfo> children = new ArrayList<CanvasViewInfo>();
+ for (CanvasViewInfo vi : mChildren) {
+ if (vi.mNodeSiblings == null) {
+ children.add(vi);
+ } else if (vi.isPrimaryNodeSibling()) {
+ children.add(vi);
+ }
+ }
+ return children;
+ }
+
+ haveHidden |= info.isHidden();
+ }
+
+ if (haveHidden) {
+ List<CanvasViewInfo> children = new ArrayList<CanvasViewInfo>(mChildren.size());
+ for (CanvasViewInfo vi : mChildren) {
+ if (!vi.isHidden()) {
+ children.add(vi);
+ }
+ }
+
+ return children;
+ }
+
+ return mChildren;
+ }
+
+ /**
+ * Returns true if the specific {@link CanvasViewInfo} is a parent
+ * of this {@link CanvasViewInfo}. It can be a direct parent or any
+ * grand-parent higher in the hierarchy.
+ *
+ * @param potentialParent the view info to check
+ * @return true if the given info is a parent of this view
+ */
+ public boolean isParent(@NonNull CanvasViewInfo potentialParent) {
+ CanvasViewInfo p = mParent;
+ while (p != null) {
+ if (p == potentialParent) {
+ return true;
+ }
+ p = p.getParent();
+ }
+ return false;
+ }
+
+ /**
+ * Returns the name of the {@link CanvasViewInfo}.
+ * Could be null, although unlikely.
+ * Experience shows this is the full qualified Java name of the View.
+ * TODO: Rename this method to getFqcn.
+ *
+ * @return the name of the view info
+ *
+ * @see ViewInfo#getClassName()
+ */
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the View object associated with the {@link CanvasViewInfo}.
+ * @return the view object or null.
+ */
+ @Nullable
+ public Object getViewObject() {
+ return mViewObject;
+ }
+
+ /**
+ * Returns the baseline of this object, or -1 if it does not support a baseline
+ *
+ * @return the baseline or -1
+ */
+ public int getBaseline() {
+ if (mViewInfo != null) {
+ int baseline = mViewInfo.getBaseLine();
+ if (baseline != Integer.MIN_VALUE) {
+ return baseline;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns the {@link Margins} for this {@link CanvasViewInfo}
+ *
+ * @return the {@link Margins} for this {@link CanvasViewInfo}
+ */
+ @Nullable
+ public Margins getMargins() {
+ if (mViewInfo != null) {
+ int leftMargin = mViewInfo.getLeftMargin();
+ int topMargin = mViewInfo.getTopMargin();
+ int rightMargin = mViewInfo.getRightMargin();
+ int bottomMargin = mViewInfo.getBottomMargin();
+ return new Margins(
+ leftMargin != Integer.MIN_VALUE ? leftMargin : 0,
+ rightMargin != Integer.MIN_VALUE ? rightMargin : 0,
+ topMargin != Integer.MIN_VALUE ? topMargin : 0,
+ bottomMargin != Integer.MIN_VALUE ? bottomMargin : 0
+ );
+ }
+
+ return null;
+ }
+
+ // ---- Implementation of IPropertySource
+ // TODO: Get rid of this once the old propertysheet implementation is fully gone
+
+ @Override
+ public Object getEditableValue() {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ return ((IPropertySource) uiView).getEditableValue();
+ }
+ return null;
+ }
+
+ @Override
+ public IPropertyDescriptor[] getPropertyDescriptors() {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ return ((IPropertySource) uiView).getPropertyDescriptors();
+ }
+ return null;
+ }
+
+ @Override
+ public Object getPropertyValue(Object id) {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ return ((IPropertySource) uiView).getPropertyValue(id);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isPropertySet(Object id) {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ return ((IPropertySource) uiView).isPropertySet(id);
+ }
+ return false;
+ }
+
+ @Override
+ public void resetPropertyValue(Object id) {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ ((IPropertySource) uiView).resetPropertyValue(id);
+ }
+ }
+
+ @Override
+ public void setPropertyValue(Object id, Object value) {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ ((IPropertySource) uiView).setPropertyValue(id, value);
+ }
+ }
+
+ /**
+ * Returns the XML node corresponding to this info, or null if there is no
+ * such XML node.
+ *
+ * @return The XML node corresponding to this info object, or null
+ */
+ @Nullable
+ public Node getXmlNode() {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ return uiView.getXmlNode();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns true iff this view info corresponds to a root element.
+ *
+ * @return True iff this is a root view info.
+ */
+ public boolean isRoot() {
+ // Select the visual element -- unless it's the root.
+ // The root element is the one whose GRAND parent
+ // is null (because the parent will be a -document-
+ // node).
+
+ // Special case: a gesture overlay is sometimes added as the root, but for all intents
+ // and purposes it is its layout child that is the real root so treat that one as the
+ // root as well (such that the whole layout canvas does not highlight as part of hovers
+ // etc)
+ if (mParent != null
+ && mParent.mName.endsWith(GESTURE_OVERLAY_VIEW)
+ && mParent.isRoot()
+ && mParent.mChildren.size() == 1) {
+ return true;
+ }
+
+ return mUiViewNode == null || mUiViewNode.getUiParent() == null ||
+ mUiViewNode.getUiParent().getUiParent() == null;
+ }
+
+ /**
+ * Returns true if this {@link CanvasViewInfo} represents an invisible widget that
+ * should be highlighted when selected. This is the case for any layout that is less than the minimum
+ * threshold ({@link #SELECTION_MIN_SIZE}), or any other view that has -0- bounds.
+ *
+ * @return True if this is a tiny layout or invisible view
+ */
+ public boolean isInvisible() {
+ if (isHidden()) {
+ // Don't expand and highlight hidden widgets
+ return false;
+ }
+
+ if (mAbsRect.width < SELECTION_MIN_SIZE || mAbsRect.height < SELECTION_MIN_SIZE) {
+ return mUiViewNode != null && (mUiViewNode.getDescriptor().hasChildren() ||
+ mAbsRect.width <= 0 || mAbsRect.height <= 0);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if this {@link CanvasViewInfo} represents a widget that should be
+ * hidden, such as a {@code <Space>} which are typically not manipulated by the user
+ * through dragging etc.
+ *
+ * @return true if this is a hidden view
+ */
+ public boolean isHidden() {
+ if (GridLayoutRule.sDebugGridLayout) {
+ return false;
+ }
+
+ return FQCN_SPACE.equals(mName) || FQCN_SPACE_V7.equals(mName);
+ }
+
+ /**
+ * Is this {@link CanvasViewInfo} a view that has had its padding inflated in order to
+ * make it visible during selection or dragging? Note that this is NOT considered to
+ * be the case in the explode-all-views mode where all nodes have their padding
+ * increased; it's only used for views that individually exploded because they were
+ * requested visible and they returned true for {@link #isInvisible()}.
+ *
+ * @return True if this is an exploded node.
+ */
+ public boolean isExploded() {
+ return mExploded;
+ }
+
+ /**
+ * Mark this {@link CanvasViewInfo} as having been exploded or not. See the
+ * {@link #isExploded()} method for details on what this property means.
+ *
+ * @param exploded New value of the exploded property to mark this info with.
+ */
+ void setExploded(boolean exploded) {
+ mExploded = exploded;
+ }
+
+ /**
+ * Returns the info represented as a {@link SimpleElement}.
+ *
+ * @return A {@link SimpleElement} wrapping this info.
+ */
+ @NonNull
+ SimpleElement toSimpleElement() {
+
+ UiViewElementNode uiNode = getUiViewNode();
+
+ String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor());
+ String parentFqcn = null;
+ Rect bounds = SwtUtils.toRect(getAbsRect());
+ Rect parentBounds = null;
+
+ UiElementNode uiParent = uiNode.getUiParent();
+ if (uiParent != null) {
+ parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor());
+ }
+ if (getParent() != null) {
+ parentBounds = SwtUtils.toRect(getParent().getAbsRect());
+ }
+
+ SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds);
+
+ for (UiAttributeNode attr : uiNode.getAllUiAttributes()) {
+ String value = attr.getCurrentValue();
+ if (value != null && value.length() > 0) {
+ AttributeDescriptor attrDesc = attr.getDescriptor();
+ SimpleAttribute a = new SimpleAttribute(
+ attrDesc.getNamespaceUri(),
+ attrDesc.getXmlLocalName(),
+ value);
+ e.addAttribute(a);
+ }
+ }
+
+ for (CanvasViewInfo childVi : getChildren()) {
+ SimpleElement e2 = childVi.toSimpleElement();
+ if (e2 != null) {
+ e.addInnerElement(e2);
+ }
+ }
+
+ return e;
+ }
+
+ /**
+ * Returns the layout url attribute value for the closest surrounding include or
+ * fragment element parent, or null if this {@link CanvasViewInfo} is not rendered as
+ * part of an include or fragment tag.
+ *
+ * @return the layout url attribute value for the surrounding include tag, or null if
+ * not applicable
+ */
+ @Nullable
+ public String getIncludeUrl() {
+ CanvasViewInfo curr = this;
+ while (curr != null) {
+ if (curr.mUiViewNode != null) {
+ Node node = curr.mUiViewNode.getXmlNode();
+ if (node != null && node.getNodeType() == Node.ELEMENT_NODE) {
+ String nodeName = node.getNodeName();
+ if (node.getNamespaceURI() == null
+ && SdkConstants.VIEW_INCLUDE.equals(nodeName)) {
+ // Note: the layout attribute is NOT in the Android namespace
+ Element element = (Element) node;
+ String url = element.getAttribute(SdkConstants.ATTR_LAYOUT);
+ if (url.length() > 0) {
+ return url;
+ }
+ } else if (SdkConstants.VIEW_FRAGMENT.equals(nodeName)) {
+ String url = FragmentMenu.getFragmentLayout(node);
+ if (url != null) {
+ return url;
+ }
+ }
+ }
+ }
+ curr = curr.mParent;
+ }
+
+ return null;
+ }
+
+ /** Adds the given {@link CanvasViewInfo} as a new last child of this view */
+ private void addChild(@NonNull CanvasViewInfo child) {
+ mChildren.add(child);
+ }
+
+ /** Adds the given {@link CanvasViewInfo} as a child at the given index */
+ private void addChildAt(int index, @NonNull CanvasViewInfo child) {
+ mChildren.add(index, child);
+ }
+
+ /**
+ * Removes the given {@link CanvasViewInfo} from the child list of this view, and
+ * returns true if it was successfully removed
+ *
+ * @param child the child to be removed
+ * @return true if it was a child and was removed
+ */
+ public boolean removeChild(@NonNull CanvasViewInfo child) {
+ return mChildren.remove(child);
+ }
+
+ @Override
+ public String toString() {
+ return "CanvasViewInfo [name=" + mName + ", node=" + mUiViewNode + "]";
+ }
+
+ // ---- Factory functionality ----
+
+ /**
+ * Creates a new {@link CanvasViewInfo} hierarchy based on the given {@link ViewInfo}
+ * hierarchy. Note that this will not necessarily create one {@link CanvasViewInfo}
+ * for each {@link ViewInfo}. It will generally only create {@link CanvasViewInfo}
+ * objects for {@link ViewInfo} objects that contain a reference to an
+ * {@link UiViewElementNode}, meaning that it corresponds to an element in the XML
+ * file for this layout file. This is not always the case, such as in the following
+ * scenarios:
+ * <ul>
+ * <li>we link to other layouts with {@code <include>}
+ * <li>the current view is rendered within another view ("Show Included In") such that
+ * the outer file does not correspond to elements in the current included XML layout
+ * <li>on older platforms that don't support {@link Capability#EMBEDDED_LAYOUT} there
+ * is no reference to the {@code <include>} tag
+ * <li>with the {@code <merge>} tag we don't get a reference to the corresponding
+ * element
+ * <ul>
+ * <p>
+ * This method will build up a set of {@link CanvasViewInfo} that corresponds to the
+ * actual <b>selectable</b> views (which are also shown in the Outline).
+ *
+ * @param layoutlib5 if true, the {@link ViewInfo} hierarchy was created by layoutlib
+ * version 5 or higher, which means this algorithm can make certain assumptions
+ * (for example that {@code <merge>} siblings will provide {@link MergeCookie}
+ * references, so we don't have to search for them.)
+ * @param root the root {@link ViewInfo} to build from
+ * @return a {@link CanvasViewInfo} hierarchy
+ */
+ @NonNull
+ public static Pair<CanvasViewInfo,List<Rectangle>> create(ViewInfo root, boolean layoutlib5) {
+ return new Builder(layoutlib5).create(root);
+ }
+
+ /** Builder object which walks over a tree of {@link ViewInfo} objects and builds
+ * up a corresponding {@link CanvasViewInfo} hierarchy. */
+ private static class Builder {
+ public Builder(boolean layoutlib5) {
+ mLayoutLib5 = layoutlib5;
+ }
+
+ /**
+ * The mapping from nodes that have a {@code <merge>} as a parent in the node
+ * model to their corresponding views
+ */
+ private Map<UiViewElementNode, List<CanvasViewInfo>> mMergeNodeMap;
+
+ /**
+ * Whether the ViewInfos are provided by a layout library that is version 5 or
+ * later, since that will allow us to take several shortcuts
+ */
+ private boolean mLayoutLib5;
+
+ /**
+ * Creates a hierarchy of {@link CanvasViewInfo} objects and merge bounding
+ * rectangles from the given {@link ViewInfo} hierarchy
+ */
+ private Pair<CanvasViewInfo,List<Rectangle>> create(ViewInfo root) {
+ Object cookie = root.getCookie();
+ if (cookie == null) {
+ // Special case: If the root-most view does not have a view cookie,
+ // then we are rendering some outer layout surrounding this layout, and in
+ // that case we must search down the hierarchy for the (possibly multiple)
+ // sub-roots that correspond to elements in this layout, and place them inside
+ // an outer view that has no node. In the outline this item will be used to
+ // show the inclusion-context.
+ CanvasViewInfo rootView = createView(null, root, 0, 0);
+ addKeyedSubtrees(rootView, root, 0, 0);
+
+ List<Rectangle> includedBounds = new ArrayList<Rectangle>();
+ for (CanvasViewInfo vi : rootView.getChildren()) {
+ if (vi.getNodeSiblings() == null || vi.isPrimaryNodeSibling()) {
+ includedBounds.add(vi.getAbsRect());
+ }
+ }
+
+ // There are <merge> nodes here; see if we can insert it into the hierarchy
+ if (mMergeNodeMap != null) {
+ // Locate all the nodes that have a <merge> as a parent in the node model,
+ // and where the view sits at the top level inside the include-context node.
+ UiViewElementNode merge = null;
+ List<CanvasViewInfo> merged = new ArrayList<CanvasViewInfo>();
+ for (Map.Entry<UiViewElementNode, List<CanvasViewInfo>> entry : mMergeNodeMap
+ .entrySet()) {
+ UiViewElementNode node = entry.getKey();
+ if (!hasMergeParent(node)) {
+ continue;
+ }
+ List<CanvasViewInfo> views = entry.getValue();
+ assert views.size() > 0;
+ CanvasViewInfo view = views.get(0); // primary
+ if (view.getParent() != rootView) {
+ continue;
+ }
+ UiElementNode parent = node.getUiParent();
+ if (merge != null && parent != merge) {
+ continue;
+ }
+ merge = (UiViewElementNode) parent;
+ merged.add(view);
+ }
+ if (merged.size() > 0) {
+ // Compute a bounding box for the merged views
+ Rectangle absRect = null;
+ for (CanvasViewInfo child : merged) {
+ Rectangle rect = child.getAbsRect();
+ if (absRect == null) {
+ absRect = rect;
+ } else {
+ absRect = absRect.union(rect);
+ }
+ }
+
+ CanvasViewInfo mergeView = new CanvasViewInfo(rootView, VIEW_MERGE, null,
+ merge, absRect, absRect, null /* viewInfo */);
+ for (CanvasViewInfo view : merged) {
+ if (rootView.removeChild(view)) {
+ mergeView.addChild(view);
+ }
+ }
+ rootView.addChild(mergeView);
+ }
+ }
+
+ return Pair.of(rootView, includedBounds);
+ } else {
+ // We have a view key at the top, so just go and create {@link CanvasViewInfo}
+ // objects for each {@link ViewInfo} until we run into a null key.
+ CanvasViewInfo rootView = addKeyedSubtrees(null, root, 0, 0);
+
+ // Special case: look to see if the root element is really a <merge>, and if so,
+ // manufacture a view for it such that we can target this root element
+ // in drag & drop operations, such that we can show it in the outline, etc
+ if (rootView != null && hasMergeParent(rootView.getUiViewNode())) {
+ CanvasViewInfo merge = new CanvasViewInfo(null, VIEW_MERGE, null,
+ (UiViewElementNode) rootView.getUiViewNode().getUiParent(),
+ rootView.getAbsRect(), rootView.getSelectionRect(),
+ null /* viewInfo */);
+ // Insert the <merge> as the new real root
+ rootView.mParent = merge;
+ merge.addChild(rootView);
+ rootView = merge;
+ }
+
+ return Pair.of(rootView, null);
+ }
+ }
+
+ private boolean hasMergeParent(UiViewElementNode rootNode) {
+ UiElementNode rootParent = rootNode.getUiParent();
+ return (rootParent instanceof UiViewElementNode
+ && VIEW_MERGE.equals(rootParent.getDescriptor().getXmlName()));
+ }
+
+ /** Creates a {@link CanvasViewInfo} for a given {@link ViewInfo} but does not recurse */
+ private CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX,
+ int parentY) {
+ Object cookie = root.getCookie();
+ UiViewElementNode node = null;
+ if (cookie instanceof UiViewElementNode) {
+ node = (UiViewElementNode) cookie;
+ } else if (cookie instanceof MergeCookie) {
+ cookie = ((MergeCookie) cookie).getCookie();
+ if (cookie instanceof UiViewElementNode) {
+ node = (UiViewElementNode) cookie;
+ CanvasViewInfo view = createView(parent, root, parentX, parentY, node);
+ if (root.getCookie() instanceof MergeCookie && view.mNodeSiblings == null) {
+ List<CanvasViewInfo> v = mMergeNodeMap == null ?
+ null : mMergeNodeMap.get(node);
+ if (v != null) {
+ v.add(view);
+ } else {
+ v = new ArrayList<CanvasViewInfo>();
+ v.add(view);
+ if (mMergeNodeMap == null) {
+ mMergeNodeMap =
+ new HashMap<UiViewElementNode, List<CanvasViewInfo>>();
+ }
+ mMergeNodeMap.put(node, v);
+ }
+ view.mNodeSiblings = v;
+ }
+
+ return view;
+ }
+ }
+
+ return createView(parent, root, parentX, parentY, node);
+ }
+
+ /**
+ * Creates a {@link CanvasViewInfo} for a given {@link ViewInfo} but does not recurse.
+ * This method specifies an explicit {@link UiViewElementNode} to use rather than
+ * relying on the view cookie in the info object.
+ */
+ private CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX,
+ int parentY, UiViewElementNode node) {
+
+ int x = root.getLeft();
+ int y = root.getTop();
+ int w = root.getRight() - x;
+ int h = root.getBottom() - y;
+
+ x += parentX;
+ y += parentY;
+
+ Rectangle absRect = new Rectangle(x, y, w - 1, h - 1);
+
+ if (w < SELECTION_MIN_SIZE) {
+ int d = (SELECTION_MIN_SIZE - w) / 2;
+ x -= d;
+ w += SELECTION_MIN_SIZE - w;
+ }
+
+ if (h < SELECTION_MIN_SIZE) {
+ int d = (SELECTION_MIN_SIZE - h) / 2;
+ y -= d;
+ h += SELECTION_MIN_SIZE - h;
+ }
+
+ Rectangle selectionRect = new Rectangle(x, y, w - 1, h - 1);
+
+ return new CanvasViewInfo(parent, root.getClassName(), root.getViewObject(), node,
+ absRect, selectionRect, root);
+ }
+
+ /** Create a subtree recursively until you run out of keys */
+ private CanvasViewInfo createSubtree(CanvasViewInfo parent, ViewInfo viewInfo,
+ int parentX, int parentY) {
+ assert viewInfo.getCookie() != null;
+
+ CanvasViewInfo view = createView(parent, viewInfo, parentX, parentY);
+ // Bug workaround: Ensure that we never have a child node identical
+ // to its parent node: this can happen for example when rendering a
+ // ZoomControls view where the merge cookies point to the parent.
+ if (parent != null && view.mUiViewNode == parent.mUiViewNode) {
+ return null;
+ }
+
+ // Process children:
+ parentX += viewInfo.getLeft();
+ parentY += viewInfo.getTop();
+
+ List<ViewInfo> children = viewInfo.getChildren();
+
+ if (mLayoutLib5) {
+ for (ViewInfo child : children) {
+ Object cookie = child.getCookie();
+ if (cookie instanceof UiViewElementNode || cookie instanceof MergeCookie) {
+ CanvasViewInfo childView = createSubtree(view, child,
+ parentX, parentY);
+ if (childView != null) {
+ view.addChild(childView);
+ }
+ } // else: null cookies, adapter item references, etc: No child views.
+ }
+
+ return view;
+ }
+
+ // See if we have any missing keys at this level
+ int missingNodes = 0;
+ int mergeNodes = 0;
+ for (ViewInfo child : children) {
+ // Only use children which have a ViewKey of the correct type.
+ // We can't interact with those when they have a null key or
+ // an incompatible type.
+ Object cookie = child.getCookie();
+ if (!(cookie instanceof UiViewElementNode)) {
+ if (cookie instanceof MergeCookie) {
+ mergeNodes++;
+ } else {
+ missingNodes++;
+ }
+ }
+ }
+
+ if (missingNodes == 0 && mergeNodes == 0) {
+ // No missing nodes; this is the normal case, and we can just continue to
+ // recursively add our children
+ for (ViewInfo child : children) {
+ CanvasViewInfo childView = createSubtree(view, child,
+ parentX, parentY);
+ view.addChild(childView);
+ }
+
+ // TBD: Emit placeholder views for keys that have no views?
+ } else {
+ // We don't have keys for one or more of the ViewInfos. There are many
+ // possible causes: we are on an SDK platform that does not support
+ // embedded_layout rendering, or we are including a view with a <merge>
+ // as the root element.
+
+ UiViewElementNode uiViewNode = view.getUiViewNode();
+ String containerName = uiViewNode != null
+ ? uiViewNode.getDescriptor().getXmlLocalName() : ""; //$NON-NLS-1$
+ if (containerName.equals(SdkConstants.VIEW_INCLUDE)) {
+ // This is expected -- we don't WANT to get node keys for the content
+ // of an include since it's in a different file and should be treated
+ // as a single unit that cannot be edited (hence, no CanvasViewInfo
+ // children)
+ } else {
+ // We are getting children with null keys where we don't expect it;
+ // this usually means that we are dealing with an Android platform
+ // that does not support {@link Capability#EMBEDDED_LAYOUT}, or
+ // that there are <merge> tags which are doing surprising things
+ // to the view hierarchy
+ LinkedList<UiViewElementNode> unused = new LinkedList<UiViewElementNode>();
+ if (uiViewNode != null) {
+ for (UiElementNode child : uiViewNode.getUiChildren()) {
+ if (child instanceof UiViewElementNode) {
+ unused.addLast((UiViewElementNode) child);
+ }
+ }
+ }
+ for (ViewInfo child : children) {
+ Object cookie = child.getCookie();
+ if (mergeNodes > 0 && cookie instanceof MergeCookie) {
+ cookie = ((MergeCookie) cookie).getCookie();
+ }
+ if (cookie != null) {
+ unused.remove(cookie);
+ }
+ }
+
+ if (unused.size() > 0 || mergeNodes > 0) {
+ if (unused.size() == missingNodes) {
+ // The number of unmatched elements and ViewInfos are identical;
+ // it's very likely that they match one to one, so just use these
+ for (ViewInfo child : children) {
+ if (child.getCookie() == null) {
+ // Only create a flat (non-recursive) view
+ CanvasViewInfo childView = createView(view, child, parentX,
+ parentY, unused.removeFirst());
+ view.addChild(childView);
+ } else {
+ CanvasViewInfo childView = createSubtree(view, child, parentX,
+ parentY);
+ view.addChild(childView);
+ }
+ }
+ } else {
+ // We have an uneven match. In this case we might be dealing
+ // with <merge> etc.
+ // We have no way to associate elements back with the
+ // corresponding <include> tags if there are more than one of
+ // them. That's not a huge tragedy since visually you are not
+ // allowed to edit these anyway; we just need to make a visual
+ // block for these for selection and outline purposes.
+ addMismatched(view, parentX, parentY, children, unused);
+ }
+ } else {
+ // No unused keys, but there are views without keys.
+ // We can't represent these since all views must have node keys
+ // such that you can operate on them. Just ignore these.
+ for (ViewInfo child : children) {
+ if (child.getCookie() != null) {
+ CanvasViewInfo childView = createSubtree(view, child,
+ parentX, parentY);
+ view.addChild(childView);
+ }
+ }
+ }
+ }
+ }
+
+ return view;
+ }
+
+ /**
+ * We have various {@link ViewInfo} children with null keys, and/or nodes in
+ * the corresponding UI model that are not referenced by any of the {@link ViewInfo}
+ * objects. This method attempts to account for this, by matching the views in
+ * the right order.
+ */
+ private void addMismatched(CanvasViewInfo parentView, int parentX, int parentY,
+ List<ViewInfo> children, LinkedList<UiViewElementNode> unused) {
+ UiViewElementNode afterNode = null;
+ UiViewElementNode beforeNode = null;
+ // We have one important clue we can use when matching unused nodes
+ // with views: if we have a view V1 with node N1, and a view V2 with node N2,
+ // then we can only match unknown node UN with unknown node UV if
+ // V1 < UV < V2 and N1 < UN < N2.
+ // We can use these constraints to do the matching, for example by
+ // a simple DAG traversal. However, since the number of unmatched nodes
+ // will typically be very small, we'll just do a simple algorithm here
+ // which checks forwards/backwards whether a match is valid.
+ for (int index = 0, size = children.size(); index < size; index++) {
+ ViewInfo child = children.get(index);
+ if (child.getCookie() != null) {
+ CanvasViewInfo childView = createSubtree(parentView, child, parentX, parentY);
+ if (childView != null) {
+ parentView.addChild(childView);
+ }
+ if (child.getCookie() instanceof UiViewElementNode) {
+ afterNode = (UiViewElementNode) child.getCookie();
+ }
+ } else {
+ beforeNode = nextViewNode(children, index);
+
+ // Find first eligible node from unused
+ // TOD: What if there are more eligible? We need to process ALL views
+ // and all nodes in one go here
+
+ UiViewElementNode matching = null;
+ for (UiViewElementNode candidate : unused) {
+ if (afterNode == null || isAfter(afterNode, candidate)) {
+ if (beforeNode == null || isBefore(beforeNode, candidate)) {
+ matching = candidate;
+ break;
+ }
+ }
+ }
+
+ if (matching != null) {
+ unused.remove(matching);
+ CanvasViewInfo childView = createView(parentView, child, parentX, parentY,
+ matching);
+ parentView.addChild(childView);
+ afterNode = matching;
+ } else {
+ // We have no node for the view -- what do we do??
+ // Nothing - we only represent stuff in the outline that is in the
+ // source model, not in the render
+ }
+ }
+ }
+
+ // Add zero-bounded boxes for all remaining nodes since they need to show
+ // up in the outline, need to be selectable so you can press Delete, etc.
+ if (unused.size() > 0) {
+ Map<UiViewElementNode, Integer> rankMap =
+ new HashMap<UiViewElementNode, Integer>();
+ Map<UiViewElementNode, CanvasViewInfo> infoMap =
+ new HashMap<UiViewElementNode, CanvasViewInfo>();
+ UiElementNode parent = unused.get(0).getUiParent();
+ if (parent != null) {
+ int index = 0;
+ for (UiElementNode child : parent.getUiChildren()) {
+ UiViewElementNode node = (UiViewElementNode) child;
+ rankMap.put(node, index++);
+ }
+ for (CanvasViewInfo child : parentView.getChildren()) {
+ infoMap.put(child.getUiViewNode(), child);
+ }
+ List<Integer> usedIndexes = new ArrayList<Integer>();
+ for (UiViewElementNode node : unused) {
+ Integer rank = rankMap.get(node);
+ if (rank != null) {
+ usedIndexes.add(rank);
+ }
+ }
+ Collections.sort(usedIndexes);
+ for (int i = usedIndexes.size() - 1; i >= 0; i--) {
+ Integer rank = usedIndexes.get(i);
+ UiViewElementNode found = null;
+ for (UiViewElementNode node : unused) {
+ if (rankMap.get(node) == rank) {
+ found = node;
+ break;
+ }
+ }
+ if (found != null) {
+ Rectangle absRect = new Rectangle(parentX, parentY, 0, 0);
+ String name = found.getDescriptor().getXmlLocalName();
+ CanvasViewInfo v = new CanvasViewInfo(parentView, name, null, found,
+ absRect, absRect, null /* viewInfo */);
+ // Find corresponding index in the parent view
+ List<CanvasViewInfo> siblings = parentView.getChildren();
+ int insertPosition = siblings.size();
+ for (int j = siblings.size() - 1; j >= 0; j--) {
+ CanvasViewInfo sibling = siblings.get(j);
+ UiViewElementNode siblingNode = sibling.getUiViewNode();
+ if (siblingNode != null) {
+ Integer siblingRank = rankMap.get(siblingNode);
+ if (siblingRank != null && siblingRank < rank) {
+ insertPosition = j + 1;
+ break;
+ }
+ }
+ }
+ parentView.addChildAt(insertPosition, v);
+ unused.remove(found);
+ }
+ }
+ }
+ // Add in any remaining
+ for (UiViewElementNode node : unused) {
+ Rectangle absRect = new Rectangle(parentX, parentY, 0, 0);
+ String name = node.getDescriptor().getXmlLocalName();
+ CanvasViewInfo v = new CanvasViewInfo(parentView, name, null, node, absRect,
+ absRect, null /* viewInfo */);
+ parentView.addChild(v);
+ }
+ }
+ }
+
+ private boolean isBefore(UiViewElementNode beforeNode, UiViewElementNode candidate) {
+ UiElementNode parent = candidate.getUiParent();
+ if (parent != null) {
+ for (UiElementNode sibling : parent.getUiChildren()) {
+ if (sibling == beforeNode) {
+ return false;
+ } else if (sibling == candidate) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean isAfter(UiViewElementNode afterNode, UiViewElementNode candidate) {
+ UiElementNode parent = candidate.getUiParent();
+ if (parent != null) {
+ for (UiElementNode sibling : parent.getUiChildren()) {
+ if (sibling == afterNode) {
+ return true;
+ } else if (sibling == candidate) {
+ return false;
+ }
+ }
+ }
+ return false;
+ }
+
+ private UiViewElementNode nextViewNode(List<ViewInfo> children, int index) {
+ int size = children.size();
+ for (; index < size; index++) {
+ ViewInfo child = children.get(index);
+ if (child.getCookie() instanceof UiViewElementNode) {
+ return (UiViewElementNode) child.getCookie();
+ }
+ }
+
+ return null;
+ }
+
+ /** Search for a subtree with valid keys and add those subtrees */
+ private CanvasViewInfo addKeyedSubtrees(CanvasViewInfo parent, ViewInfo viewInfo,
+ int parentX, int parentY) {
+ // We don't include MergeCookies when searching down for the first non-null key,
+ // since this means we are in a "Show Included In" context, and the include tag itself
+ // (which the merge cookie is pointing to) is still in the including-document rather
+ // than the included document. Therefore, we only accept real UiViewElementNodes here,
+ // not MergeCookies.
+ if (viewInfo.getCookie() != null) {
+ CanvasViewInfo subtree = createSubtree(parent, viewInfo, parentX, parentY);
+ if (parent != null && subtree != null) {
+ parent.mChildren.add(subtree);
+ }
+ return subtree;
+ } else {
+ for (ViewInfo child : viewInfo.getChildren()) {
+ addKeyedSubtrees(parent, child, parentX + viewInfo.getLeft(), parentY
+ + viewInfo.getTop());
+ }
+
+ return null;
+ }
+ }
+ }
+}