blob: 03c6c3926f530c070400c6ce8a2065bbcfc490ed [file] [log] [blame]
/*
* 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;
}
}
}
}