diff options
11 files changed, 1033 insertions, 222 deletions
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 0000000..77f5c22 --- /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 <merge>} tag + */ +public class MergeRule extends FrameLayoutRule { + // The <merge> tag behaves a lot like the FrameLayout; all children are added + // on top of each other at (0,0) + + @Override + public List<MenuAction> getContextMenu(INode selectedNode) { + // Deliberately ignore super.getContextMenu(); we don't want to attempt to list + // properties for the <merge> 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 84647bd..0585f4f 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<CanvasViewInfo> mChildren = new ArrayList<CanvasViewInfo>(); /** @@ -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 <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, @@ -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 <merge>} 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 <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 + */ + public List<CanvasViewInfo> 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. + * <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 + */ + public List<CanvasViewInfo> getUniqueChildren() { + 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; + } + } + + 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<CanvasViewInfo,List<Rectangle>> 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<UiViewElementNode,List<CanvasViewInfo>> mMergeNodeMap; + + public 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.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 <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); + } + } - 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 <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()); + // Insert the <merge> 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<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); } - 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<ViewInfo> 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<ViewInfo> 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 <merge> - // 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 <merge> tags which are doing surprising things - // to the view hierarchy - LinkedList<UiViewElementNode> unused = new LinkedList<UiViewElementNode>(); - 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 <merge> + // 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 <merge> tags which are doing surprising things + // to the view hierarchy + LinkedList<UiViewElementNode> unused = new LinkedList<UiViewElementNode>(); + 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 <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) { - // 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 <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. - 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<ViewInfo> 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<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); + 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<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); + // 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); + 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<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; } - } - /** 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 8e94358..cb5c849 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 84f3e01..66adad8 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 16f6fba..07ab41c 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 d7bc412..02f98b1 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<CanvasViewInfo> children = ((CanvasViewInfo) element).getChildren(); + List<CanvasViewInfo> 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 90aeebf..817f2f9 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<CanvasViewInfo> 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 738da30..01487b9 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. * <p/> * This <em>can</em> 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}. * <p/> * 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<ViewInfo> rootList = session.getRootViews(); - if (rootList != null && rootList.size() > 0) { - root = rootList.get(0); + Pair<CanvasViewInfo,List<Rectangle>> infos = null; + + if (rootList == null || rootList.size() == 0) { + // Special case: Look to see if this is really an empty <merge> 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<UiElementNode> 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 + * <merge>} tag at the root + * + * @return true if there is a {@code <merge>} at the root of this editor's document + */ + private boolean hasMergeRoot() { + UiDocumentNode model = mCanvas.getLayoutEditor().getUiRootNode(); + if (model != null) { + List<UiElementNode> 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. * <p/> @@ -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<Rectangle>(); - } - 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 <merge> 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<CanvasViewInfo> 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 c5dbec5..fa05c37 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 (<merge>) + 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 657a7f8..547db8b 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<ViewInfo> children = new ArrayList<ViewInfo>(); + // Should be matched up with corresponding node: + Set<Integer> missingKeys = new HashSet<Integer>(); + // Should not be matched with any views, but should get view created: + Set<Integer> extraKeys = new HashSet<Integer>(); + // Should not be matched with any nodes + Set<Integer> extraViews = new HashSet<Integer>(); + 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<String> expectedViewNames = new ArrayList<String>(); + List<String> expectedNodeNames = new ArrayList<String>(); + + 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<MergeCookie> cookies = new LinkedList<MergeCookie>(); + for (int i = 0; i < 10; i++) { + UiViewElementNode node = createNode(rootNode, "childNode" + i, false); + cookies.addFirst(new MergeCookie(node)); + } + Iterator<MergeCookie> it = cookies.iterator(); + ArrayList<ViewInfo> children = new ArrayList<ViewInfo>(); + 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<ViewInfo> children = new ArrayList<ViewInfo>(); + 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<CanvasViewInfo, List<Rectangle>> result = CanvasViewInfo.create(root); + CanvasViewInfo rootView = result.getFirst(); + List<Rectangle> 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<CanvasViewInfo> nodeSiblings = childView.getNodeSiblings(); + assertEquals(5, nodeSiblings.size()); + } + List<CanvasViewInfo> 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<ViewInfo> children = new ArrayList<ViewInfo>(); + 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<CanvasViewInfo, List<Rectangle>> result = CanvasViewInfo.create(root); + CanvasViewInfo rootView = result.getFirst(); + List<Rectangle> 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<CanvasViewInfo> nodeSiblings = childView.getNodeSiblings(); + assertEquals(5, nodeSiblings.size()); + } + List<CanvasViewInfo> 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 08f191a..277089f 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); |