diff options
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.java | 1178 |
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; + } + } + } +} |