From: Tor Norbye Date: Mon, 24 Jan 2011 17:12:47 +0000 (-0800) Subject: Cluster of improvements for merge tag views X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=9155df4effaf2079215d5f77dc2e70dd145a6fdf;p=android-x86%2Fsdk.git Cluster of improvements for merge tag views This changeset contains various improvements around usage of the tag. Some of these fixes require layoutlib 5. * Use the new layoutlib support for rendering multiple children at the root level - they now show up in the Outline (provided you are running layoutlib 5), can be selected in the layout editor, etc. * Add a drop handler such that you can drag into the view and get drop feedback (similar to the FrameLayout) * If the is empty, we don't get any ViewInfos, so in that case manufacture a dummy view sized to the screen. Similarly, if we get back ViewInfos that are children of a tag in the UI model, create a view initialized to the bounding rectangle of these views and reparent the views to it. * Support highlighting multiple views simultaneously when you select an include tag that renders into multiple views (because the root of the included layout was a tag). Similarly, make "Show Included In" work properly for views, and make the overlay mask used to hide all included content also reveal only the primary selected views (when a view is included more than once.) (Also tweak the visual appearance of the mask, and use better icon for the view root in the included-root scenario.) * Improve the algorithm which deals with render results with null keys. Use adjacent children that -do- have keys as constraints when attempting to match up views without keys and unreferenced model nodes. This fixes issue http://code.google.com/p/android/issues/detail?id=14188 * Improve the way we pick views under the mouse. This used to search down the view hierarchy in sibling order. Instead, search in reverse sibling order since this will match what is drawn in the layout. For views like FrameLayout and views, the children are painted on top of ech other, so clicking on whatever is on top should choose that view, not some earlier sibling below it. * Fix such that when you drag into the canvas, we *always* target the root node, even if it is not under the mouse. This is particularly important with tags, but this also helps if you for example have a LinearLayout as the root element, and the layout_height property is wrap_content instead of match_parent. In that case, the LinearLayout will *only* cover its children, so if you drag over the visual screen, it looks like you should be able to drop into the layout, but you cannot since it only covers its children. With this fix, all positions outside the root element's actual bounds are also considered targetting the root. * Fix broken unit test, add new unit tests. Change-Id: Id96a06a8763d02845af4531a47fe32afe703df2f --- diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MergeRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MergeRule.java new file mode 100644 index 000000000..77f5c22cf --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MergeRule.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2011 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.common.layout; + +import com.android.ide.common.api.INode; +import com.android.ide.common.api.MenuAction; + +import java.util.List; + +/** + * Drop handler for the {@code } tag + */ +public class MergeRule extends FrameLayoutRule { + // The tag behaves a lot like the FrameLayout; all children are added + // on top of each other at (0,0) + + @Override + public List getContextMenu(INode selectedNode) { + // Deliberately ignore super.getContextMenu(); we don't want to attempt to list + // properties for the tag + return null; + } +} 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 index 84647bdce..0585f4fc0 100755 --- 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 @@ -16,8 +16,11 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_MERGE; + import com.android.ide.common.api.Rect; 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; @@ -25,6 +28,7 @@ import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDes 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.util.Pair; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.ui.views.properties.IPropertyDescriptor; @@ -34,8 +38,11 @@ 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. @@ -66,7 +73,7 @@ public class CanvasViewInfo implements IPropertySource { private final String mName; private final Object mViewObject; private final UiViewElementNode mUiViewNode; - private final CanvasViewInfo mParent; + private CanvasViewInfo mParent; private final ArrayList mChildren = new ArrayList(); /** @@ -77,6 +84,17 @@ public class CanvasViewInfo implements IPropertySource { private boolean mExploded; /** + * Node sibling. This is usually null, but it's possible for a single node in the + * model to have multiple separate views in the canvas, for example + * when you {@code } a view that has multiple widgets inside a + * {@code } 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 circular list of all the siblings that share the same node. + */ + private List mNodeSiblings; + + /** * Constructs a {@link CanvasViewInfo} initialized with the given initial values. */ private CanvasViewInfo(CanvasViewInfo parent, String name, @@ -142,6 +160,77 @@ public class CanvasViewInfo implements IPropertySource { } /** + * For nodes that have multiple views rendered from a single node, such as the + * children of a {@code } tag included into a separate layout, return the + * "primary" view, the first view that is rendered + */ + 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 will include this + * view). For most views this is going to be null, but for views that share a + * single node (such as widgets inside a {@code } 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 + */ + public List getNodeSiblings() { + return mNodeSiblings; + } + + /** + * Returns all the children of the canvas view info where each child corresponds to a + * unique node. This is intended for use by the outline for example, where only the + * actual nodes are displayed, not the views themselves. + *

+ * 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 } tag, where all these widgets have the + * same node (the {@code } tag). + * + * @return list of {@link CanvasViewInfo} objects that are children of this view, + * never null + */ + public List getUniqueChildren() { + for (CanvasViewInfo info : mChildren) { + if (info.mNodeSiblings != null) { + // We have secondary children; must create a new collection containing + // only non-secondary children + List children = new ArrayList(); + for (CanvasViewInfo vi : mChildren) { + if (vi.mNodeSiblings == null) { + children.add(vi); + } else if (vi.isPrimaryNodeSibling()) { + 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. @@ -376,6 +465,32 @@ public class CanvasViewInfo implements IPropertySource { return null; } + /** Adds the given {@link CanvasViewInfo} as a new last child of this view */ + private void addChild(CanvasViewInfo child) { + mChildren.add(child); + } + + /** Adds the given {@link CanvasViewInfo} as a child at the given index */ + private void addChildAt(int index, 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(CanvasViewInfo child) { + return mChildren.remove(child); + } + + @Override + public String toString() { + return "CanvasViewInfo [name=" + mName + ", node=" + mUiViewNode + "]"; + } + // ---- Factory functionality ---- /** @@ -402,234 +517,488 @@ public class CanvasViewInfo implements IPropertySource { * @param root the root {@link ViewInfo} to build from * @return a {@link CanvasViewInfo} hierarchy */ - public static CanvasViewInfo create(ViewInfo root) { - if (root.getCookie() == 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); - return rootView; - } 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. - return addKeyedSubtrees(null, root, 0, 0); - } + public static Pair> create(ViewInfo root) { + return new Builder().create(root); } - /** Creates a {@link CanvasViewInfo} for a given {@link ViewInfo} but does not recurse */ - private static CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX, - int parentY) { - Object cookie = root.getCookie(); - UiViewElementNode node = null; - if (cookie instanceof UiViewElementNode) { - node = (UiViewElementNode) cookie; - } - - return createView(parent, root, parentX, parentY, node); - } + /** Builder object which walks over a tree of {@link ViewInfo} objects and builds + * up a corresponding {@link CanvasViewInfo} hierarchy. */ + private static class Builder { + private Map> mMergeNodeMap; + + public Pair> 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 includedBounds = new ArrayList(); + for (CanvasViewInfo vi : rootView.getChildren()) { + if (vi.isPrimaryNodeSibling()) { + includedBounds.add(vi.getAbsRect()); + } + } - /** - * 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 static CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX, - int parentY, UiViewElementNode node) { + // There are nodes here; see if we can insert it into the hierarchy + if (mMergeNodeMap != null) { + // Locate all the nodes that have a 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 merged = new ArrayList(); + for (Map.Entry> entry : mMergeNodeMap + .entrySet()) { + UiViewElementNode node = entry.getKey(); + if (!hasMergeParent(node)) { + continue; + } + List 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); + } + } - int x = root.getLeft(); - int y = root.getTop(); - int w = root.getRight() - x; - int h = root.getBottom() - y; + CanvasViewInfo mergeView = new CanvasViewInfo(rootView, VIEW_MERGE, null, + merge, absRect, absRect); + for (CanvasViewInfo view : merged) { + if (rootView.removeChild(view)) { + mergeView.addChild(view); + } + } + rootView.addChild(mergeView); + } + } - x += parentX; - y += parentY; + 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 , 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()); + // Insert the as the new real root + rootView.mParent = merge; + merge.addChild(rootView); + rootView = merge; + } - Rectangle absRect = new Rectangle(x, y, w - 1, h - 1); + return Pair.of(rootView, null); + } + } - if (w < SELECTION_MIN_SIZE) { - int d = (SELECTION_MIN_SIZE - w) / 2; - x -= d; - w += SELECTION_MIN_SIZE - w; + private boolean hasMergeParent(UiViewElementNode rootNode) { + UiElementNode rootParent = rootNode.getUiParent(); + return (rootParent instanceof UiViewElementNode + && VIEW_MERGE.equals(rootParent.getDescriptor().getXmlName())); } - if (h < SELECTION_MIN_SIZE) { - int d = (SELECTION_MIN_SIZE - h) / 2; - y -= d; - h += SELECTION_MIN_SIZE - h; + /** 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 v = mMergeNodeMap == null ? + null : mMergeNodeMap.get(node); + if (v != null) { + v.add(view); + } else { + v = new ArrayList(); + v.add(view); + if (mMergeNodeMap == null) { + mMergeNodeMap = + new HashMap>(); + } + mMergeNodeMap.put(node, v); + } + view.mNodeSiblings = v; + } + + return view; + } + } + + return createView(parent, root, parentX, parentY, node); } - Rectangle selectionRect = new Rectangle(x, y, w - 1, h - 1); + /** + * 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) { - return new CanvasViewInfo(parent, root.getClassName(), root.getViewObject(), node, absRect, - selectionRect); - } + int x = root.getLeft(); + int y = root.getTop(); + int w = root.getRight() - x; + int h = root.getBottom() - y; - /** Create a subtree recursively until you run out of keys */ - private static CanvasViewInfo createSubtree(CanvasViewInfo parent, ViewInfo viewInfo, - int parentX, int parentY) { - assert viewInfo.getCookie() != null; + x += parentX; + y += parentY; - CanvasViewInfo view = createView(parent, viewInfo, parentX, parentY); + Rectangle absRect = new Rectangle(x, y, w - 1, h - 1); - // Process children: - parentX += viewInfo.getLeft(); - parentY += viewInfo.getTop(); + if (w < SELECTION_MIN_SIZE) { + int d = (SELECTION_MIN_SIZE - w) / 2; + x -= d; + w += SELECTION_MIN_SIZE - w; + } - // See if we have any missing keys at this level - int missingNodes = 0; - List children = viewInfo.getChildren(); - 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)) { - missingNodes++; + 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); } - if (missingNodes == 0) { - // No missing nodes; this is the normal case, and we can just continue to - // recursively add our children + /** 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); + + // Process children: + parentX += viewInfo.getLeft(); + parentY += viewInfo.getTop(); + + // See if we have any missing keys at this level + int missingNodes = 0; + int mergeNodes = 0; + List children = viewInfo.getChildren(); for (ViewInfo child : children) { - CanvasViewInfo childView = createSubtree(view, child, parentX, parentY); - view.addChild(childView); - } - } 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 - // as the root element. - - String containerName = view.getUiViewNode().getDescriptor().getXmlLocalName(); - if (containerName.equals(LayoutDescriptors.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 tags which are doing surprising things - // to the view hierarchy - LinkedList unused = new LinkedList(); - for (UiElementNode child : view.getUiViewNode().getUiChildren()) { - if (child instanceof UiViewElementNode) { - unused.addLast((UiViewElementNode) child); + // 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) { - Object cookie = child.getCookie(); - if (cookie != null) { - unused.remove(cookie); - } + CanvasViewInfo childView = createSubtree(view, child, + parentX, parentY); + view.addChild(childView); } - if (unused.size() > 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 + + // 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 + // as the root element. + + String containerName = view.getUiViewNode().getDescriptor().getXmlLocalName(); + if (containerName.equals(LayoutDescriptors.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 tags which are doing surprising things + // to the view hierarchy + LinkedList unused = new LinkedList(); + for (UiElementNode child : view.getUiViewNode().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 etc. + // We have no way to associate elements back with the + // corresponding 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) { - // 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); + if (child.getCookie() != null) { + CanvasViewInfo childView = createSubtree(view, child, + parentX, parentY); view.addChild(childView); } } - } else { - // We have an uneven match. In this case we might be dealing - // with etc. - // We have no way to associate elements back with the - // corresponding 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. - UiViewElementNode reference = unused.get(0); - addBoundingView(view, children, reference, parentX, parentY); } } } - } - return view; - } + return view; + } - /** - * Add a single bounding view for all the non-keyed children with dimensions that span - * the bounding rectangle of all these children, and associate it with the given node - * reference. Keyed children are added in the normal way. - */ - private static void addBoundingView(CanvasViewInfo parentView, List children, - UiViewElementNode reference, int parentX, int parentY) { - Rectangle absRect = null; - int insertIndex = -1; - for (int index = 0, size = children.size(); index < size; index++) { - ViewInfo child = children.get(index); - if (child.getCookie() == null) { - int x = child.getLeft(); - int y = child.getTop(); - int width = child.getRight() - x; - int height = child.getBottom() - y; - Rectangle rect = new Rectangle(x, y, width, height); - if (absRect == null) { - absRect = rect; - insertIndex = index; + /** + * 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 children, LinkedList 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); + parentView.addChild(childView); + if (child.getCookie() instanceof UiViewElementNode) { + afterNode = (UiViewElementNode) child.getCookie(); + } } else { - absRect = absRect.union(rect); + 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 rankMap = + new HashMap(); + Map infoMap = + new HashMap(); + 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 usedIndexes = new ArrayList(); + 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); + // Find corresponding index in the parent view + List 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); + parentView.addChild(v); } - } else { - CanvasViewInfo childView = createSubtree(parentView, child, parentX, parentY); - parentView.addChild(childView); } } - if (absRect != null) { - absRect.x += parentX; - absRect.y += parentY; - String name = reference.getDescriptor().getXmlLocalName(); - CanvasViewInfo childView = new CanvasViewInfo(parentView, name, null, reference, - absRect, absRect); - parentView.addChild(childView, insertIndex); + + 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; } - } - /** Search for a subtree with valid keys and add those subtrees */ - private static CanvasViewInfo addKeyedSubtrees(CanvasViewInfo parent, ViewInfo viewInfo, - int parentX, int parentY) { - if (viewInfo.getCookie() != null) { - CanvasViewInfo subtree = createSubtree(parent, viewInfo, parentX, parentY); + private boolean isAfter(UiViewElementNode afterNode, UiViewElementNode candidate) { + UiElementNode parent = candidate.getUiParent(); if (parent != null) { - parent.mChildren.add(subtree); + for (UiElementNode sibling : parent.getUiChildren()) { + if (sibling == afterNode) { + return true; + } else if (sibling == candidate) { + return false; + } + } } - return subtree; - } else { - for (ViewInfo child : viewInfo.getChildren()) { - addKeyedSubtrees(parent, child, parentX + viewInfo.getLeft(), parentY - + viewInfo.getTop()); + return false; + } + + private UiViewElementNode nextViewNode(List 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; } - } - /** Adds the given {@link CanvasViewInfo} as a new last child of this view */ - private void addChild(CanvasViewInfo child) { - mChildren.add(child); - } + /** 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) { + parent.mChildren.add(subtree); + } + return subtree; + } else { + for (ViewInfo child : viewInfo.getChildren()) { + addKeyedSubtrees(parent, child, parentX + viewInfo.getLeft(), parentY + + viewInfo.getTop()); + } - /** Adds the given {@link CanvasViewInfo} as a new child at the given index */ - private void addChild(CanvasViewInfo child, int index) { - if (index < 0) { - index = mChildren.size(); + return null; + } } - mChildren.add(index, child); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java index 8e943583a..cb5c849c0 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java @@ -1760,6 +1760,9 @@ public class GraphicalEditorPart extends EditorPart * Called when the file changes triggered a redraw of the layout */ public void reloadLayout(final ChangeFlags flags, final boolean libraryChanged) { + if (mConfigComposite.isDisposed()) { + return; + } Display display = mConfigComposite.getDisplay(); display.asyncExec(new Runnable() { public void run() { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java index 84f3e011f..66adad8a0 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java @@ -35,7 +35,7 @@ import java.util.List; */ public class IncludeOverlay extends Overlay { /** Mask transparency - 0 is transparent, 255 is opaque */ - private static final int MASK_TRANSPARENCY = 208; + private static final int MASK_TRANSPARENCY = 160; /** The associated {@link LayoutCanvas}. */ private LayoutCanvas mCanvas; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java index 16f6fba9d..07ab41cf0 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java @@ -521,6 +521,15 @@ public class MoveGesture extends DropGesture { vi = mCurrentView; } else { vi = mCanvas.getViewHierarchy().findViewInfoAt(p); + + // When dragging into the canvas, if you are not over any other view, target + // the root element (since it may not "fill" the screen, e.g. if you have a linear + // layout but have layout_height wrap_content, then the layout will only extend + // to cover the children in the layout, not the whole visible screen area, which + // may be surprising + if (vi == null) { + vi = mCanvas.getViewHierarchy().getRoot(); + } } boolean isMove = true; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java index d7bc4120f..02f98b151 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java @@ -407,7 +407,7 @@ public class OutlinePage extends ContentOutlinePage } } if (element instanceof CanvasViewInfo) { - List children = ((CanvasViewInfo) element).getChildren(); + List children = ((CanvasViewInfo) element).getUniqueChildren(); if (children != null) { return children.toArray(); } @@ -497,6 +497,8 @@ public class OutlinePage extends ContentOutlinePage element = vi.getUiViewNode(); } + Image image = getImage(element); + if (element instanceof UiElementNode) { UiElementNode node = (UiElementNode) element; styledString = node.getStyledDescription(); @@ -549,6 +551,7 @@ public class OutlinePage extends ContentOutlinePage if (includedWithin != null) { styledString = new StyledString(); styledString.append(includedWithin.getDisplayName(), QUALIFIER_STYLER); + image = IconFactory.getInstance().getIcon(LayoutDescriptors.VIEW_INCLUDE); } } @@ -559,7 +562,7 @@ public class OutlinePage extends ContentOutlinePage cell.setText(styledString.toString()); cell.setStyleRanges(styledString.getStyleRanges()); - cell.setImage(getImage(element)); + cell.setImage(image); super.update(cell); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java index 90aeebf8d..817f2f9d2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java @@ -63,7 +63,7 @@ public class SelectionOverlay extends Overlay { NodeProxy node = s.getNode(); if (node != null) { String name = s.getName(); - paintSelection(gcWrapper, node, name, isMultipleSelection); + paintSelection(gcWrapper, s.getViewInfo(), node, name, isMultipleSelection); } } @@ -121,7 +121,7 @@ public class SelectionOverlay extends Overlay { } /** Called by the canvas when a view is being selected. */ - private void paintSelection(IGraphics gc, INode selectedNode, String displayName, + private void paintSelection(IGraphics gc, CanvasViewInfo view, INode selectedNode, String displayName, boolean isMultipleSelection) { Rect r = selectedNode.getBounds(); @@ -133,6 +133,18 @@ public class SelectionOverlay extends Overlay { gc.fillRect(r); gc.drawRect(r); + // Paint sibling rectangles, if applicable + List siblings = view.getNodeSiblings(); + if (siblings != null) { + for (CanvasViewInfo sibling : siblings) { + if (sibling != view) { + r = SwtUtils.toRect(sibling.getSelectionRect()); + gc.fillRect(r); + gc.drawRect(r); + } + } + } + /* Label hidden pending selection visual design if (displayName == null || isMultipleSelection) { return; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java index 738da30a6..01487b953 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java @@ -16,21 +16,27 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_MERGE; + import com.android.ide.common.api.INode; import com.android.ide.common.rendering.api.RenderSession; import com.android.ide.common.rendering.api.ViewInfo; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.util.Pair; import org.eclipse.swt.graphics.Rectangle; import org.w3c.dom.Node; +import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.RandomAccess; import java.util.Set; /** @@ -52,7 +58,7 @@ public class ViewHierarchy { } /** - * The CanvasViewInfo root created by the last call to {@link #setResult} + * The CanvasViewInfo root created by the last call to {@link #setSession} * with a valid layout. *

* This can be null to indicate we're dealing with an empty document with @@ -63,7 +69,7 @@ public class ViewHierarchy { private CanvasViewInfo mLastValidViewInfoRoot; /** - * True when the last {@link #setResult} provided a valid {@link LayoutScene}. + * True when the last {@link #setSession} provided a valid {@link LayoutScene}. *

* When false this means the canvas is displaying an out-dated result image & bounds and some * features should be disabled accordingly such a drag'n'drop. @@ -137,21 +143,42 @@ public class ViewHierarchy { mSession = session; mIsResultValid = (session != null && session.getResult().isSuccess()); mExplodedParents = false; - mIncludedBounds = null; - if (mIsResultValid && session != null) { - ViewInfo root = null; - List rootList = session.getRootViews(); - if (rootList != null && rootList.size() > 0) { - root = rootList.get(0); + Pair> infos = null; + + if (rootList == null || rootList.size() == 0) { + // Special case: Look to see if this is really an empty view, + // which shows up without any ViewInfos in the merge. In that case we + // want to manufacture an empty view, such that we can target the view + // via drag & drop, etc. + if (hasMergeRoot()) { + ViewInfo mergeRoot = createMergeInfo(session); + infos = CanvasViewInfo.create(mergeRoot); + } else { + infos = null; + } + } else { + if (rootList.size() > 1 && hasMergeRoot()) { + ViewInfo mergeRoot = createMergeInfo(session); + mergeRoot.setChildren(rootList); + infos = CanvasViewInfo.create(mergeRoot); + } else { + ViewInfo root = rootList.get(0); + if (root != null) { + infos = CanvasViewInfo.create(root); + } else { + infos = null; + } + } } - - if (root == null) { - mLastValidViewInfoRoot = null; + if (infos != null) { + mLastValidViewInfoRoot = infos.getFirst(); + mIncludedBounds = infos.getSecond(); } else { - mLastValidViewInfoRoot = CanvasViewInfo.create(root); + mLastValidViewInfoRoot = null; + mIncludedBounds = null; } updateNodeProxies(mLastValidViewInfoRoot, null); @@ -167,10 +194,41 @@ public class ViewHierarchy { // Update the selection mCanvas.getSelectionManager().sync(mLastValidViewInfoRoot); } else { + mIncludedBounds = null; mInvisibleParents.clear(); } } + private ViewInfo createMergeInfo(RenderSession session) { + BufferedImage image = session.getImage(); + ControlPoint imageSize = ControlPoint.create(mCanvas, + ICanvasTransform.IMAGE_MARGIN + image.getWidth(), + ICanvasTransform.IMAGE_MARGIN + image.getHeight()); + LayoutPoint layoutSize = imageSize.toLayout(); + UiDocumentNode model = mCanvas.getLayoutEditor().getUiRootNode(); + List children = model.getUiChildren(); + return new ViewInfo(VIEW_MERGE, children.get(0), 0, 0, layoutSize.x, layoutSize.y); + } + + /** + * Returns true if this view hierarchy corresponds to an editor that has a {@code + * } tag at the root + * + * @return true if there is a {@code } at the root of this editor's document + */ + private boolean hasMergeRoot() { + UiDocumentNode model = mCanvas.getLayoutEditor().getUiRootNode(); + if (model != null) { + List children = model.getUiChildren(); + if (children != null && children.size() > 0 + && VIEW_MERGE.equals(children.get(0).getDescriptor().getXmlName())) { + return true; + } + } + + return false; + } + /** * Creates or updates the node proxy for this canvas view info. *

@@ -189,14 +247,6 @@ public class ViewHierarchy { if (key != null) { mCanvas.getNodeFactory().create(vi); - - if (parentKey == null && vi.getParent() != null) { - // This is an included view root - if (mIncludedBounds == null) { - mIncludedBounds = new ArrayList(); - } - mIncludedBounds.add(vi.getAbsRect()); - } } for (CanvasViewInfo child : vi.getChildren()) { @@ -411,7 +461,15 @@ public class ViewHierarchy { if (r.contains(p.x, p.y)) { // try to find a matching child first - for (CanvasViewInfo child : canvasViewInfo.getChildren()) { + // Iterate in REVERSE z order such that siblings on top + // are checked before earlier siblings (this matters in layouts like + // FrameLayout and in contexts where the views are sitting on top + // of each other and we want to select the same view as the one drawn + // on top of the others + List children = canvasViewInfo.getChildren(); + assert children instanceof RandomAccess; + for (int i = children.size() - 1; i >= 0; i--) { + CanvasViewInfo child = children.get(i); CanvasViewInfo v = findViewInfoAt_Recursive(p, child); if (v != null) { return v; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java index c5dbec5e1..fa05c379f 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java @@ -16,6 +16,8 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gre; +import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_MERGE; + import com.android.ide.common.api.DropFeedback; import com.android.ide.common.api.IClientRulesEngine; import com.android.ide.common.api.IDragElement; @@ -614,6 +616,7 @@ public class RulesEngine { String ruleClassName; ClassLoader classLoader; if (realFqcn.startsWith("android.") || //$NON-NLS-1$ + realFqcn.equals(VIEW_MERGE) || // FIXME: Remove this special case as soon as we pull // the MapViewRule out of this code base and bundle it // with the add ons @@ -628,6 +631,10 @@ public class RulesEngine { classLoader = RulesEngine.class.getClassLoader(); int dotIndex = realFqcn.lastIndexOf('.'); String baseName = realFqcn.substring(dotIndex+1); + // Capitalize rule class name to match naming conventions, if necessary () + if (Character.isLowerCase(baseName.charAt(0))) { + baseName = Character.toUpperCase(baseName.charAt(0)) + baseName.substring(1); + } ruleClassName = packageName + "." + //$NON-NLS-1$ baseName + "Rule"; //$NON-NLS-1$ diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfoTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfoTest.java index 657a7f81e..547db8bb6 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfoTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfoTest.java @@ -17,15 +17,23 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; 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.descriptors.ElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; +import com.android.util.Pair; import org.eclipse.swt.graphics.Rectangle; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; import junit.framework.TestCase; @@ -66,7 +74,7 @@ public class CanvasViewInfoTest extends TestCase { ViewInfo child2 = new ViewInfo("Button", child2Node, 0, 20, 70, 25); root.setChildren(Arrays.asList(child1, child2)); - CanvasViewInfo rootView = CanvasViewInfo.create(root); + CanvasViewInfo rootView = CanvasViewInfo.create(root).getFirst(); assertNotNull(rootView); assertEquals("LinearLayout", rootView.getName()); assertEquals(new Rectangle(10, 10, 89, 89), rootView.getAbsRect()); @@ -103,7 +111,7 @@ public class CanvasViewInfoTest extends TestCase { ViewInfo child21 = new ViewInfo("RadioButton", child21Node, 0, 20, 70, 25); child2.setChildren(Arrays.asList(child21)); - CanvasViewInfo rootView = CanvasViewInfo.create(root); + CanvasViewInfo rootView = CanvasViewInfo.create(root).getFirst(); assertNotNull(rootView); assertEquals("LinearLayout", rootView.getName()); assertEquals(new Rectangle(10, 10, 89, 89), rootView.getAbsRect()); @@ -140,7 +148,7 @@ public class CanvasViewInfoTest extends TestCase { ViewInfo child21 = new ViewInfo("RadioButton", null, 0, 20, 70, 25); child2.setChildren(Arrays.asList(child21)); - CanvasViewInfo rootView = CanvasViewInfo.create(root); + CanvasViewInfo rootView = CanvasViewInfo.create(root).getFirst(); assertNotNull(rootView); assertEquals("LinearLayout", rootView.getName()); assertEquals(new Rectangle(10, 10, 89, 89), rootView.getAbsRect()); @@ -181,7 +189,7 @@ public class CanvasViewInfoTest extends TestCase { ViewInfo child21 = new ViewInfo("RadioButton", null, 0, 20, 70, 25); child2.setChildren(Arrays.asList(child21)); - CanvasViewInfo rootView = CanvasViewInfo.create(root); + CanvasViewInfo rootView = CanvasViewInfo.create(root).getFirst(); assertNotNull(rootView); assertEquals("LinearLayout", rootView.getName()); assertEquals(new Rectangle(10, 10, 89, 89), rootView.getAbsRect()); @@ -207,6 +215,57 @@ public class CanvasViewInfoTest extends TestCase { assertEquals(0, includedView.getChildren().size()); } + public void testMergeMatching() throws Exception { + // Test rendering of MULTIPLE included views or when there is no simple match + // between view info and ui element node children + + UiViewElementNode rootNode = createNode("android.widget.LinearLayout", true); + ViewInfo root = new ViewInfo("LinearLayout", rootNode, 10, 10, 100, 100); + UiViewElementNode child1Node = createNode(rootNode, "android.widget.Button", false); + ViewInfo child1 = new ViewInfo("CheckBox", child1Node, 0, 0, 50, 20); + UiViewElementNode multiChildNode1 = createNode(rootNode, "foo", true); + UiViewElementNode multiChildNode2 = createNode(rootNode, "bar", true); + ViewInfo child2 = new ViewInfo("RelativeLayout", null, 0, 20, 70, 25); + ViewInfo child3 = new ViewInfo("AbsoluteLayout", null, 10, 40, 50, 15); + root.setChildren(Arrays.asList(child1, child2, child3)); + ViewInfo child21 = new ViewInfo("RadioButton", null, 0, 20, 70, 25); + child2.setChildren(Arrays.asList(child21)); + + CanvasViewInfo rootView = CanvasViewInfo.create(root).getFirst(); + assertNotNull(rootView); + assertEquals("LinearLayout", rootView.getName()); + assertEquals(new Rectangle(10, 10, 89, 89), rootView.getAbsRect()); + assertEquals(new Rectangle(10, 10, 89, 89), rootView.getSelectionRect()); + assertNull(rootView.getParent()); + assertSame(rootNode, rootView.getUiViewNode()); + assertEquals(3, rootView.getChildren().size()); + + CanvasViewInfo childView1 = rootView.getChildren().get(0); + CanvasViewInfo includedView1 = rootView.getChildren().get(1); + CanvasViewInfo includedView2 = rootView.getChildren().get(2); + + assertEquals("CheckBox", childView1.getName()); + assertSame(rootView, childView1.getParent()); + assertEquals(new Rectangle(10, 10, 49, 19), childView1.getAbsRect()); + assertEquals(new Rectangle(10, 10, 49, 19), childView1.getSelectionRect()); + assertSame(childView1.getUiViewNode(), child1Node); + + assertEquals("RelativeLayout", includedView1.getName()); + assertSame(multiChildNode1, includedView1.getUiViewNode()); + assertEquals("foo", includedView1.getUiViewNode().getDescriptor().getXmlName()); + assertSame(multiChildNode2, includedView2.getUiViewNode()); + assertEquals("AbsoluteLayout", includedView2.getName()); + assertEquals("bar", includedView2.getUiViewNode().getDescriptor().getXmlName()); + assertSame(rootView, includedView1.getParent()); + assertSame(rootView, includedView2.getParent()); + assertEquals(new Rectangle(10, 30, 69, 4), includedView1.getAbsRect()); + assertEquals(new Rectangle(10, 30, 69, 5), includedView1.getSelectionRect()); + assertEquals(new Rectangle(20, 50, 39, -26), includedView2.getAbsRect()); + assertEquals(new Rectangle(20, 35, 39, 5), includedView2.getSelectionRect()); + assertEquals(0, includedView1.getChildren().size()); + assertEquals(0, includedView2.getChildren().size()); + } + public void testMerge() throws Exception { // Test rendering of MULTIPLE included views or when there is no simple match // between view info and ui element node children @@ -222,7 +281,7 @@ public class CanvasViewInfoTest extends TestCase { ViewInfo child21 = new ViewInfo("RadioButton", null, 0, 20, 70, 25); child2.setChildren(Arrays.asList(child21)); - CanvasViewInfo rootView = CanvasViewInfo.create(root); + CanvasViewInfo rootView = CanvasViewInfo.create(root).getFirst(); assertNotNull(rootView); assertEquals("LinearLayout", rootView.getName()); assertEquals(new Rectangle(10, 10, 89, 89), rootView.getAbsRect()); @@ -240,14 +299,249 @@ public class CanvasViewInfoTest extends TestCase { assertEquals(new Rectangle(10, 10, 49, 19), childView1.getSelectionRect()); assertSame(childView1.getUiViewNode(), child1Node); - assertEquals("foo", includedView.getName()); + assertEquals("RelativeLayout", includedView.getName()); assertSame(rootView, includedView.getParent()); - assertEquals(new Rectangle(10, 30, 70, 5), includedView.getAbsRect()); - assertEquals(new Rectangle(10, 30, 70, 5), includedView.getSelectionRect()); + assertEquals(new Rectangle(10, 30, 69, 4), includedView.getAbsRect()); + assertEquals(new Rectangle(10, 30, 69, 5), includedView.getSelectionRect()); assertEquals(0, includedView.getChildren().size()); assertSame(multiChildNode, includedView.getUiViewNode()); } + public void testInsertMerge() throws Exception { + // Test rendering of MULTIPLE included views or when there is no simple match + // between view info and ui element node children + + UiViewElementNode mergeNode = createNode("merge", true); + UiViewElementNode rootNode = createNode(mergeNode, "android.widget.Button", false); + ViewInfo root = new ViewInfo("Button", rootNode, 10, 10, 100, 100); + + CanvasViewInfo rootView = CanvasViewInfo.create(root).getFirst(); + assertNotNull(rootView); + assertEquals("merge", rootView.getName()); + assertSame(rootView.getUiViewNode(), mergeNode); + assertEquals(new Rectangle(10, 10, 89, 89), rootView.getAbsRect()); + assertEquals(new Rectangle(10, 10, 89, 89), rootView.getSelectionRect()); + assertNull(rootView.getParent()); + assertSame(mergeNode, rootView.getUiViewNode()); + assertEquals(1, rootView.getChildren().size()); + + CanvasViewInfo childView1 = rootView.getChildren().get(0); + + assertEquals("Button", childView1.getName()); + assertSame(rootView, childView1.getParent()); + assertEquals(new Rectangle(10, 10, 89, 89), childView1.getAbsRect()); + assertEquals(new Rectangle(10, 10, 89, 89), childView1.getSelectionRect()); + assertSame(childView1.getUiViewNode(), rootNode); + } + + public void testUnmatchedMissing() throws Exception { + UiViewElementNode rootNode = createNode("android.widget.LinearLayout", true); + ViewInfo root = new ViewInfo("LinearLayout", rootNode, 0, 0, 100, 100); + List children = new ArrayList(); + // Should be matched up with corresponding node: + Set missingKeys = new HashSet(); + // Should not be matched with any views, but should get view created: + Set extraKeys = new HashSet(); + // Should not be matched with any nodes + Set extraViews = new HashSet(); + int numViews = 30; + missingKeys.add(0); + missingKeys.add(4); + missingKeys.add(14); + missingKeys.add(29); + extraKeys.add(9); + extraKeys.add(20); + extraKeys.add(22); + extraViews.add(18); + extraViews.add(24); + + List expectedViewNames = new ArrayList(); + List expectedNodeNames = new ArrayList(); + + for (int i = 0; i < numViews; i++) { + UiViewElementNode childNode = null; + if (!extraViews.contains(i)) { + childNode = createNode(rootNode, "childNode" + i, false); + } + Object cookie = missingKeys.contains(i) || extraViews.contains(i) ? null : childNode; + ViewInfo childView = new ViewInfo("childView" + i, cookie, + 0, i * 20, 50, (i + 1) * 20); + children.add(childView); + + if (!extraViews.contains(i)) { + expectedViewNames.add("childView" + i); + expectedNodeNames.add("childNode" + i); + } + + if (extraKeys.contains(i)) { + createNode(rootNode, "extraNodeAt" + i, false); + + expectedViewNames.add("extraNodeAt" + i); + expectedNodeNames.add("extraNodeAt" + i); + } + } + root.setChildren(children); + + CanvasViewInfo rootView = CanvasViewInfo.create(root).getFirst(); + assertNotNull(rootView); + + // dump(root, 0); + // dump(rootView, 0); + + assertEquals("LinearLayout", rootView.getName()); + assertNull(rootView.getParent()); + assertSame(rootNode, rootView.getUiViewNode()); + assertEquals(numViews + extraKeys.size() - extraViews.size(), rootNode.getUiChildren() + .size()); + assertEquals(numViews + extraKeys.size() - extraViews.size(), + rootView.getChildren().size()); + assertEquals(expectedViewNames.size(), rootView.getChildren().size()); + for (int i = 0, n = rootView.getChildren().size(); i < n; i++) { + CanvasViewInfo childView = rootView.getChildren().get(i); + String expectedViewName = expectedViewNames.get(i); + String expectedNodeName = expectedNodeNames.get(i); + assertEquals(expectedViewName, childView.getName()); + assertNotNull(childView.getUiViewNode()); + assertEquals(expectedNodeName, childView.getUiViewNode().getDescriptor().getXmlName()); + } + } + + public void testMergeCookies() throws Exception { + UiViewElementNode rootNode = createNode("android.widget.LinearLayout", true); + ViewInfo root = new ViewInfo("LinearLayout", rootNode, 0, 0, 100, 100); + + // Create the merge cookies in the opposite order to ensure that we don't + // apply our own logic when matching up views with nodes + LinkedList cookies = new LinkedList(); + for (int i = 0; i < 10; i++) { + UiViewElementNode node = createNode(rootNode, "childNode" + i, false); + cookies.addFirst(new MergeCookie(node)); + } + Iterator it = cookies.iterator(); + ArrayList children = new ArrayList(); + for (int i = 0; i < 10; i++) { + ViewInfo childView = new ViewInfo("childView" + i, it.next(), 0, i * 20, 50, + (i + 1) * 20); + children.add(childView); + } + root.setChildren(children); + + CanvasViewInfo rootView = CanvasViewInfo.create(root).getFirst(); + assertNotNull(rootView); + + assertEquals("LinearLayout", rootView.getName()); + assertNull(rootView.getParent()); + assertSame(rootNode, rootView.getUiViewNode()); + for (int i = 0, n = rootView.getChildren().size(); i < n; i++) { + CanvasViewInfo childView = rootView.getChildren().get(i); + assertEquals("childView" + i, childView.getName()); + assertEquals("childNode" + (9 - i), childView.getUiViewNode().getDescriptor() + .getXmlName()); + } + } + + public void testMergeCookies2() throws Exception { + UiViewElementNode rootNode = createNode("android.widget.LinearLayout", true); + ViewInfo root = new ViewInfo("LinearLayout", rootNode, 0, 0, 100, 100); + + UiViewElementNode node1 = createNode(rootNode, "childNode1", false); + UiViewElementNode node2 = createNode(rootNode, "childNode2", false); + MergeCookie cookie1 = new MergeCookie(node1); + MergeCookie cookie2 = new MergeCookie(node2); + + // Sets alternating merge cookies and checks whether the node sibling lists are + // okay and merged correctly + + ArrayList children = new ArrayList(); + for (int i = 0; i < 10; i++) { + Object cookie = (i % 2) == 0 ? cookie1 : cookie2; + ViewInfo childView = new ViewInfo("childView" + i, cookie, 0, i * 20, 50, (i + 1) * 20); + children.add(childView); + } + root.setChildren(children); + + Pair> result = CanvasViewInfo.create(root); + CanvasViewInfo rootView = result.getFirst(); + List bounds = result.getSecond(); + assertNull(bounds); + assertNotNull(rootView); + + assertEquals("LinearLayout", rootView.getName()); + assertNull(rootView.getParent()); + assertSame(rootNode, rootView.getUiViewNode()); + assertEquals(10, rootView.getChildren().size()); + assertEquals(2, rootView.getUniqueChildren().size()); + for (int i = 0, n = rootView.getChildren().size(); i < n; i++) { + CanvasViewInfo childView = rootView.getChildren().get(i); + assertEquals("childView" + i, childView.getName()); + Object cookie = (i % 2) == 0 ? node1 : node2; + assertSame(cookie, childView.getUiViewNode()); + List nodeSiblings = childView.getNodeSiblings(); + assertEquals(5, nodeSiblings.size()); + } + List nodeSiblings = rootView.getChildren().get(0).getNodeSiblings(); + for (int j = 0; j < 5; j++) { + assertEquals("childView" + (j * 2), nodeSiblings.get(j).getName()); + } + nodeSiblings = rootView.getChildren().get(1).getNodeSiblings(); + for (int j = 0; j < 5; j++) { + assertEquals("childView" + (j * 2 + 1), nodeSiblings.get(j).getName()); + } + } + + public void testIncludeBounds() throws Exception { + UiViewElementNode rootNode = createNode("android.widget.LinearLayout", true); + ViewInfo root = new ViewInfo("included", null, 0, 0, 100, 100); + + UiViewElementNode node1 = createNode(rootNode, "childNode1", false); + UiViewElementNode node2 = createNode(rootNode, "childNode2", false); + MergeCookie cookie1 = new MergeCookie(node1); + MergeCookie cookie2 = new MergeCookie(node2); + + // Sets alternating merge cookies and checks whether the node sibling lists are + // okay and merged correctly + + ArrayList children = new ArrayList(); + for (int i = 0; i < 10; i++) { + Object cookie = (i % 2) == 0 ? cookie1 : cookie2; + ViewInfo childView = new ViewInfo("childView" + i, cookie, 0, i * 20, 50, (i + 1) * 20); + children.add(childView); + } + root.setChildren(children); + + Pair> result = CanvasViewInfo.create(root); + CanvasViewInfo rootView = result.getFirst(); + List bounds = result.getSecond(); + assertNotNull(rootView); + + assertEquals("included", rootView.getName()); + assertNull(rootView.getParent()); + assertNull(rootView.getUiViewNode()); + assertEquals(10, rootView.getChildren().size()); + assertEquals(2, rootView.getUniqueChildren().size()); + for (int i = 0, n = rootView.getChildren().size(); i < n; i++) { + CanvasViewInfo childView = rootView.getChildren().get(i); + assertEquals("childView" + i, childView.getName()); + Object cookie = (i % 2) == 0 ? node1 : node2; + assertSame(cookie, childView.getUiViewNode()); + List nodeSiblings = childView.getNodeSiblings(); + assertEquals(5, nodeSiblings.size()); + } + List nodeSiblings = rootView.getChildren().get(0).getNodeSiblings(); + for (int j = 0; j < 5; j++) { + assertEquals("childView" + (j * 2), nodeSiblings.get(j).getName()); + } + nodeSiblings = rootView.getChildren().get(1).getNodeSiblings(); + for (int j = 0; j < 5; j++) { + assertEquals("childView" + (j * 2 + 1), nodeSiblings.get(j).getName()); + } + + // Only show the primary bounds as included + assertEquals(2, bounds.size()); + assertEquals(new Rectangle(0, 0, 49, 19), bounds.get(0)); + assertEquals(new Rectangle(0, 20, 49, 19), bounds.get(1)); + } + /** * Dumps out the given {@link ViewInfo} hierarchy to standard out. * Useful during development. @@ -261,11 +555,10 @@ public class CanvasViewInfoTest extends TestCase { System.out.println("Supports Embedded Layout=" + supportsEmbedding); System.out.println("Rendering context=" + graphicalEditor.getIncludedWithin()); dump(root, 0); - } /** Helper for {@link #dump(GraphicalEditorPart, ViewInfo)} */ - private static void dump(ViewInfo info, int depth) { + public static void dump(ViewInfo info, int depth) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < depth; i++) { sb.append(" "); @@ -285,7 +578,7 @@ public class CanvasViewInfoTest extends TestCase { sb.append(" "); UiViewElementNode node = (UiViewElementNode) cookie; sb.append("<"); - sb.append(node.getXmlNode().getNodeName()); + sb.append(node.getDescriptor().getXmlName()); sb.append("> "); } else if (cookie != null) { sb.append(" cookie=" + cookie); @@ -297,4 +590,24 @@ public class CanvasViewInfoTest extends TestCase { dump(child, depth + 1); } } + + /** Helper for {@link #dump(GraphicalEditorPart, ViewInfo)} */ + public static void dump(CanvasViewInfo info, int depth) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < depth; i++) { + sb.append(" "); + } + sb.append(info.getName()); + sb.append(" ["); + sb.append(info.getAbsRect()); + sb.append("], node="); + sb.append(info.getUiViewNode()); + + System.out.println(sb.toString()); + + for (CanvasViewInfo child : info.getChildren()) { + dump(child, depth + 1); + } + } + } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactoryTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactoryTest.java index 08f191ae4..277089fd0 100755 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactoryTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactoryTest.java @@ -48,7 +48,7 @@ public class NodeFactoryTest extends TestCase { ViewElementDescriptor ved = new ViewElementDescriptor("xml", "com.example.MyJavaClass"); UiViewElementNode uiv = new UiViewElementNode(ved); ViewInfo lvi = new ViewInfo("name", uiv, 10, 12, 110, 120); - CanvasViewInfo cvi = CanvasViewInfo.create(lvi); + CanvasViewInfo cvi = CanvasViewInfo.create(lvi).getFirst(); // Create a NodeProxy. NodeProxy proxy = m.create(cvi); @@ -95,7 +95,7 @@ public class NodeFactoryTest extends TestCase { ViewElementDescriptor ved = new ViewElementDescriptor("xml", "com.example.MyJavaClass"); UiViewElementNode uiv = new UiViewElementNode(ved); ViewInfo lvi = new ViewInfo("name", uiv, 10, 12, 110, 120); - CanvasViewInfo cvi = CanvasViewInfo.create(lvi); + CanvasViewInfo cvi = CanvasViewInfo.create(lvi).getFirst(); // NodeProxies are cached. Creating the same one twice returns the same proxy. NodeProxy proxy1 = m.create(cvi); @@ -107,7 +107,7 @@ public class NodeFactoryTest extends TestCase { ViewElementDescriptor ved = new ViewElementDescriptor("xml", "com.example.MyJavaClass"); UiViewElementNode uiv = new UiViewElementNode(ved); ViewInfo lvi = new ViewInfo("name", uiv, 10, 12, 110, 120); - CanvasViewInfo cvi = CanvasViewInfo.create(lvi); + CanvasViewInfo cvi = CanvasViewInfo.create(lvi).getFirst(); // NodeProxies are cached. Creating the same one twice returns the same proxy. NodeProxy proxy1 = m.create(cvi);