/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import static com.android.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; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; 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; import org.eclipse.ui.views.properties.IPropertySheetPage; import org.eclipse.ui.views.properties.IPropertySource; import org.w3c.dom.Element; import org.w3c.dom.Node; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; /** * Maps a {@link ViewInfo} in a structure more adapted to our needs. * The only large difference is that we keep both the original bounds of the view info * and we pre-compute the selection bounds which are absolute to the rendered image * (whereas the original bounds are relative to the parent view.) * <p/> * Each view also knows its parent and children. * <p/> * We can't alter {@link ViewInfo} as it is part of the LayoutBridge and needs to * have a fixed API. * <p/> * The view info also implements {@link IPropertySource}, which enables a linked * {@link IPropertySheetPage} to display the attributes of the selected element. * This class actually delegates handling of {@link IPropertySource} to the underlying * {@link UiViewElementNode}, if any. */ public class CanvasViewInfo implements IPropertySource { /** * Minimal size of the selection, in case an empty view or layout is selected. */ private static final int SELECTION_MIN_SIZE = 6; private final Rectangle mAbsRect; private final Rectangle mSelectionRect; private final String mName; private final Object mViewObject; private final UiViewElementNode mUiViewNode; private CanvasViewInfo mParent; private final ArrayList<CanvasViewInfo> mChildren = new ArrayList<CanvasViewInfo>(); /** * Is this view info an individually exploded view? This is the case for views * that were specially inflated by the {@link UiElementPullParser} and assigned * fixed padding because they were invisible and somebody requested visibility. */ private boolean mExploded; /** * Node sibling. This is usually null, but it's possible for a single node in the * model to have <b>multiple</b> separate views in the canvas, for example * when you {@code <include>} a view that has multiple widgets inside a * {@code <merge>} tag. In this case, all the views have the same node model, * the include tag, and selecting the include should highlight all the separate * views that are linked to this node. That's what this field is all about: it is * a <b>circular</b> list of all the siblings that share the same node. */ private List<CanvasViewInfo> mNodeSiblings; /** * Constructs a {@link CanvasViewInfo} initialized with the given initial values. */ private CanvasViewInfo(CanvasViewInfo parent, String name, Object viewObject, UiViewElementNode node, Rectangle absRect, Rectangle selectionRect) { mParent = parent; mName = name; mViewObject = viewObject; mUiViewNode = node; mAbsRect = absRect; mSelectionRect = selectionRect; } /** * Returns the original {@link ViewInfo} bounds in absolute coordinates * over the whole graphic. * * @return the bounding box in absolute coordinates */ public Rectangle getAbsRect() { return mAbsRect; } /* * Returns the absolute selection bounds of the view info as a rectangle. * The selection bounds will always have a size greater or equal to * {@link #SELECTION_MIN_SIZE}. * The width/height is inclusive (i.e. width = right-left-1). * This is in absolute "screen" coordinates (relative to the rendered bitmap). */ public Rectangle getSelectionRect() { return mSelectionRect; } /** * Returns the view node. Could be null, although unlikely. * @return An {@link UiViewElementNode} that uniquely identifies the object in the XML model. * @see ViewInfo#getCookie() */ public UiViewElementNode getUiViewNode() { return mUiViewNode; } /** * Returns the parent {@link CanvasViewInfo}. * It is null for the root and non-null for children. * * @return the parent {@link CanvasViewInfo}, which can be null */ public CanvasViewInfo getParent() { return mParent; } /** * Returns the list of children of this {@link CanvasViewInfo}. * The list is never null. It can be empty. * By contract, this.getChildren().get(0..n-1).getParent() == this. * * @return the children, never null */ public List<CanvasViewInfo> getChildren() { return mChildren; } /** * For nodes that have multiple views rendered from a single node, such as the * children of a {@code <merge>} tag included into a separate layout, return the * "primary" view, the first view that is rendered */ 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. * * @param potentialParent the view info to check * @return true if the given info is a parent of this view */ public boolean isParent(CanvasViewInfo potentialParent) { if (potentialParent == null) { } CanvasViewInfo p = mParent; while (p != null) { if (p == potentialParent) { return true; } p = p.getParent(); } return false; } /** * Returns the name of the {@link CanvasViewInfo}. * Could be null, although unlikely. * Experience shows this is the full qualified Java name of the View. * * @return the name of the view info, or null * * @see ViewInfo#getClassName() */ public String getName() { return mName; } /** * Returns the View object associated with the {@link CanvasViewInfo}. * @return the view object or null. */ public Object getViewObject() { return mViewObject; } // ---- Implementation of IPropertySource public Object getEditableValue() { UiViewElementNode uiView = getUiViewNode(); if (uiView != null) { return ((IPropertySource) uiView).getEditableValue(); } return null; } public IPropertyDescriptor[] getPropertyDescriptors() { UiViewElementNode uiView = getUiViewNode(); if (uiView != null) { return ((IPropertySource) uiView).getPropertyDescriptors(); } return null; } public Object getPropertyValue(Object id) { UiViewElementNode uiView = getUiViewNode(); if (uiView != null) { return ((IPropertySource) uiView).getPropertyValue(id); } return null; } public boolean isPropertySet(Object id) { UiViewElementNode uiView = getUiViewNode(); if (uiView != null) { return ((IPropertySource) uiView).isPropertySet(id); } return false; } public void resetPropertyValue(Object id) { UiViewElementNode uiView = getUiViewNode(); if (uiView != null) { ((IPropertySource) uiView).resetPropertyValue(id); } } public void setPropertyValue(Object id, Object value) { UiViewElementNode uiView = getUiViewNode(); if (uiView != null) { ((IPropertySource) uiView).setPropertyValue(id, value); } } /** * Returns the XML node corresponding to this info, or null if there is no * such XML node. * * @return The XML node corresponding to this info object, or null */ public Node getXmlNode() { UiViewElementNode uiView = getUiViewNode(); if (uiView != null) { return uiView.getXmlNode(); } return null; } /** * Returns true iff this view info corresponds to a root element. * * @return True iff this is a root view info. */ public boolean isRoot() { // Select the visual element -- unless it's the root. // The root element is the one whose GRAND parent // is null (because the parent will be a -document- // node). // Special case: a gesture overlay is sometimes added as the root, but for all intents // and purposes it is its layout child that is the real root so treat that one as the // root as well (such that the whole layout canvas does not highlight as part of hovers // etc) if (mParent != null && mParent.mName.endsWith("GestureOverlayView") //$NON-NLS-1$ && mParent.isRoot()) { return true; } return mUiViewNode == null || mUiViewNode.getUiParent() == null || mUiViewNode.getUiParent().getUiParent() == null; } /** * Returns true if this {@link CanvasViewInfo} represents an invisible parent - in * other words, a view that can have children, and that has zero bounds making it * effectively invisible. (We don't actually look for -0- bounds, but * bounds smaller than SELECTION_MIN_SIZE.) * * @return True if this is an invisible parent. */ public boolean isInvisibleParent() { if (mAbsRect.width < SELECTION_MIN_SIZE || mAbsRect.height < SELECTION_MIN_SIZE) { return mUiViewNode != null && mUiViewNode.getDescriptor().hasChildren(); } return false; } /** * Is this {@link CanvasViewInfo} a view that has had its padding inflated in order to * make it visible during selection or dragging? Note that this is NOT considered to * be the case in the explode-all-views mode where all nodes have their padding * increased; it's only used for views that individually exploded because they were * requested visible and they returned true for {@link #isInvisibleParent()}. * * @return True if this is an exploded node. */ public boolean isExploded() { return mExploded; } /** * Mark this {@link CanvasViewInfo} as having been exploded or not. See the * {@link #isExploded()} method for details on what this property means. * * @param exploded New value of the exploded property to mark this info with. */ /* package */ void setExploded(boolean exploded) { this.mExploded = exploded; } /** * Returns the info represented as a {@link SimpleElement}. * * @return A {@link SimpleElement} wrapping this info. */ /* package */ SimpleElement toSimpleElement() { UiViewElementNode uiNode = getUiViewNode(); String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor()); String parentFqcn = null; Rect bounds = SwtUtils.toRect(getAbsRect()); Rect parentBounds = null; UiElementNode uiParent = uiNode.getUiParent(); if (uiParent != null) { parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor()); } if (getParent() != null) { parentBounds = SwtUtils.toRect(getParent().getAbsRect()); } SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds); for (UiAttributeNode attr : uiNode.getUiAttributes()) { String value = attr.getCurrentValue(); if (value != null && value.length() > 0) { AttributeDescriptor attrDesc = attr.getDescriptor(); SimpleAttribute a = new SimpleAttribute( attrDesc.getNamespaceUri(), attrDesc.getXmlLocalName(), value); e.addAttribute(a); } } for (CanvasViewInfo childVi : getChildren()) { SimpleElement e2 = childVi.toSimpleElement(); if (e2 != null) { e.addInnerElement(e2); } } return e; } /** * Returns the layout url attribute value for the closest surrounding include element * parent, or null if this {@link CanvasViewInfo} is not rendered as part of an * include tag. * * @return the layout url attribute value for the surrounding include tag, or null if * not applicable */ public String getIncludeUrl() { CanvasViewInfo curr = this; while (curr != null) { if (curr.mUiViewNode != null) { Node node = curr.mUiViewNode.getXmlNode(); if (node != null && node.getNamespaceURI() == null && node.getNodeType() == Node.ELEMENT_NODE && LayoutDescriptors.VIEW_INCLUDE.equals(node.getNodeName())) { // Note: the layout attribute is NOT in the Android namespace Element element = (Element) node; String url = element.getAttribute(LayoutDescriptors.ATTR_LAYOUT); if (url.length() > 0) { return url; } } } curr = curr.mParent; } 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 ---- /** * Creates a new {@link CanvasViewInfo} hierarchy based on the given {@link ViewInfo} * hierarchy. Note that this will not necessarily create one {@link CanvasViewInfo} * for each {@link ViewInfo}. It will generally only create {@link CanvasViewInfo} * objects for {@link ViewInfo} objects that contain a reference to an * {@link UiViewElementNode}, meaning that it corresponds to an element in the XML * file for this layout file. This is not always the case, such as in the following * scenarios: * <ul> * <li>we link to other layouts with {@code <include>} * <li>the current view is rendered within another view ("Show Included In") such that * the outer file does not correspond to elements in the current included XML layout * <li>on older platforms that don't support {@link Capability#EMBEDDED_LAYOUT} there * is no reference to the {@code <include>} tag * <li>with the {@code <merge>} tag we don't get a reference to the corresponding * element * <ul> * <p> * This method will build up a set of {@link CanvasViewInfo} that corresponds to the * actual <b>selectable</b> views (which are also shown in the Outline). * * @param root the root {@link ViewInfo} to build from * @return a {@link CanvasViewInfo} hierarchy */ public static Pair<CanvasViewInfo,List<Rectangle>> create(ViewInfo root) { return new Builder().create(root); } /** Builder object which walks over a tree of {@link ViewInfo} objects and builds * up a corresponding {@link CanvasViewInfo} hierarchy. */ private static class Builder { 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.getNodeSiblings() == null || vi.isPrimaryNodeSibling()) { includedBounds.add(vi.getAbsRect()); } } // There are <merge> nodes here; see if we can insert it into the hierarchy if (mMergeNodeMap != null) { // Locate all the nodes that have a <merge> as a parent in the node model, // and where the view sits at the top level inside the include-context node. UiViewElementNode merge = null; List<CanvasViewInfo> merged = new ArrayList<CanvasViewInfo>(); for (Map.Entry<UiViewElementNode, List<CanvasViewInfo>> entry : mMergeNodeMap .entrySet()) { UiViewElementNode node = entry.getKey(); if (!hasMergeParent(node)) { continue; } List<CanvasViewInfo> views = entry.getValue(); assert views.size() > 0; CanvasViewInfo view = views.get(0); // primary if (view.getParent() != rootView) { continue; } UiElementNode parent = node.getUiParent(); if (merge != null && parent != merge) { continue; } merge = (UiViewElementNode) parent; merged.add(view); } if (merged.size() > 0) { // Compute a bounding box for the merged views Rectangle absRect = null; for (CanvasViewInfo child : merged) { Rectangle rect = child.getAbsRect(); if (absRect == null) { absRect = rect; } else { absRect = absRect.union(rect); } } CanvasViewInfo mergeView = new CanvasViewInfo(rootView, VIEW_MERGE, null, merge, absRect, absRect); for (CanvasViewInfo view : merged) { if (rootView.removeChild(view)) { mergeView.addChild(view); } } rootView.addChild(mergeView); } } return Pair.of(rootView, includedBounds); } else { // We have a view key at the top, so just go and create {@link CanvasViewInfo} // objects for each {@link ViewInfo} until we run into a null key. CanvasViewInfo rootView = addKeyedSubtrees(null, root, 0, 0); // Special case: look to see if the root element is really a <merge>, and if so, // manufacture a view for it such that we can target this root element // in drag & drop operations, such that we can show it in the outline, etc if (rootView != null && hasMergeParent(rootView.getUiViewNode())) { CanvasViewInfo merge = new CanvasViewInfo(null, VIEW_MERGE, null, (UiViewElementNode) rootView.getUiViewNode().getUiParent(), rootView.getAbsRect(), rootView.getSelectionRect()); // Insert the <merge> as the new real root rootView.mParent = merge; merge.addChild(rootView); rootView = merge; } return Pair.of(rootView, null); } } private boolean hasMergeParent(UiViewElementNode rootNode) { UiElementNode rootParent = rootNode.getUiParent(); return (rootParent instanceof UiViewElementNode && VIEW_MERGE.equals(rootParent.getDescriptor().getXmlName())); } /** Creates a {@link CanvasViewInfo} for a given {@link ViewInfo} but does not recurse */ private CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX, int parentY) { Object cookie = root.getCookie(); UiViewElementNode node = null; if (cookie instanceof UiViewElementNode) { node = (UiViewElementNode) cookie; } else if (cookie instanceof MergeCookie) { cookie = ((MergeCookie) cookie).getCookie(); if (cookie instanceof UiViewElementNode) { node = (UiViewElementNode) cookie; CanvasViewInfo view = createView(parent, root, parentX, parentY, node); if (root.getCookie() instanceof MergeCookie && view.mNodeSiblings == null) { List<CanvasViewInfo> v = mMergeNodeMap == null ? null : mMergeNodeMap.get(node); if (v != null) { v.add(view); } else { v = new ArrayList<CanvasViewInfo>(); v.add(view); if (mMergeNodeMap == null) { mMergeNodeMap = new HashMap<UiViewElementNode, List<CanvasViewInfo>>(); } mMergeNodeMap.put(node, v); } view.mNodeSiblings = v; } return view; } } return createView(parent, root, parentX, parentY, node); } /** * Creates a {@link CanvasViewInfo} for a given {@link ViewInfo} but does not recurse. * This method specifies an explicit {@link UiViewElementNode} to use rather than * relying on the view cookie in the info object. */ private CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX, int parentY, UiViewElementNode node) { int x = root.getLeft(); int y = root.getTop(); int w = root.getRight() - x; int h = root.getBottom() - y; x += parentX; y += parentY; Rectangle absRect = new Rectangle(x, y, w - 1, h - 1); if (w < SELECTION_MIN_SIZE) { int d = (SELECTION_MIN_SIZE - w) / 2; x -= d; w += SELECTION_MIN_SIZE - w; } if (h < SELECTION_MIN_SIZE) { int d = (SELECTION_MIN_SIZE - h) / 2; y -= d; h += SELECTION_MIN_SIZE - h; } Rectangle selectionRect = new Rectangle(x, y, w - 1, h - 1); return new CanvasViewInfo(parent, root.getClassName(), root.getViewObject(), node, absRect, selectionRect); } /** 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) { // Only use children which have a ViewKey of the correct type. // We can't interact with those when they have a null key or // an incompatible type. Object cookie = child.getCookie(); if (!(cookie instanceof UiViewElementNode)) { if (cookie instanceof MergeCookie) { mergeNodes++; } else { missingNodes++; } } } if (missingNodes == 0 && mergeNodes == 0) { // No missing nodes; this is the normal case, and we can just continue to // recursively add our children for (ViewInfo child : children) { CanvasViewInfo childView = createSubtree(view, child, parentX, parentY); view.addChild(childView); } // TBD: Emit placeholder views for keys that have no views? } else { // We don't have keys for one or more of the ViewInfos. There are many // possible causes: we are on an SDK platform that does not support // embedded_layout rendering, or we are including a view with a <merge> // as the root element. 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) { CanvasViewInfo childView = createSubtree(view, child, parentX, parentY); view.addChild(childView); } } } } } return view; } /** * We have various {@link ViewInfo} children with null keys, and/or nodes in * the corresponding UI model that are not referenced by any of the {@link ViewInfo} * objects. This method attempts to account for this, by matching the views in * the right order. */ private void addMismatched(CanvasViewInfo parentView, int parentX, int parentY, List<ViewInfo> children, LinkedList<UiViewElementNode> unused) { UiViewElementNode afterNode = null; UiViewElementNode beforeNode = null; // We have one important clue we can use when matching unused nodes // with views: if we have a view V1 with node N1, and a view V2 with node N2, // then we can only match unknown node UN with unknown node UV if // V1 < UV < V2 and N1 < UN < N2. // We can use these constraints to do the matching, for example by // a simple DAG traversal. However, since the number of unmatched nodes // will typically be very small, we'll just do a simple algorithm here // which checks forwards/backwards whether a match is valid. for (int index = 0, size = children.size(); index < size; index++) { ViewInfo child = children.get(index); if (child.getCookie() != null) { CanvasViewInfo childView = createSubtree(parentView, child, parentX, parentY); parentView.addChild(childView); if (child.getCookie() instanceof UiViewElementNode) { afterNode = (UiViewElementNode) child.getCookie(); } } else { beforeNode = nextViewNode(children, index); // Find first eligible node from unused // TOD: What if there are more eligible? We need to process ALL views // and all nodes in one go here UiViewElementNode matching = null; for (UiViewElementNode candidate : unused) { if (afterNode == null || isAfter(afterNode, candidate)) { if (beforeNode == null || isBefore(beforeNode, candidate)) { matching = candidate; break; } } } if (matching != null) { unused.remove(matching); CanvasViewInfo childView = createView(parentView, child, parentX, parentY, matching); parentView.addChild(childView); afterNode = matching; } else { // We have no node for the view -- what do we do?? // Nothing - we only represent stuff in the outline that is in the // source model, not in the render } } } // Add zero-bounded boxes for all remaining nodes since they need to show // up in the outline, need to be selectable so you can press Delete, etc. if (unused.size() > 0) { Map<UiViewElementNode, Integer> rankMap = new HashMap<UiViewElementNode, Integer>(); Map<UiViewElementNode, CanvasViewInfo> infoMap = new HashMap<UiViewElementNode, CanvasViewInfo>(); UiElementNode parent = unused.get(0).getUiParent(); if (parent != null) { int index = 0; for (UiElementNode child : parent.getUiChildren()) { UiViewElementNode node = (UiViewElementNode) child; rankMap.put(node, index++); } for (CanvasViewInfo child : parentView.getChildren()) { infoMap.put(child.getUiViewNode(), child); } List<Integer> usedIndexes = new ArrayList<Integer>(); for (UiViewElementNode node : unused) { Integer rank = rankMap.get(node); if (rank != null) { usedIndexes.add(rank); } } Collections.sort(usedIndexes); for (int i = usedIndexes.size() - 1; i >= 0; i--) { Integer rank = usedIndexes.get(i); UiViewElementNode found = null; for (UiViewElementNode node : unused) { if (rankMap.get(node) == rank) { found = node; break; } } if (found != null) { Rectangle absRect = new Rectangle(parentX, parentY, 0, 0); String name = found.getDescriptor().getXmlLocalName(); CanvasViewInfo v = new CanvasViewInfo(parentView, name, null, found, absRect, absRect); // 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); } } } private boolean isBefore(UiViewElementNode beforeNode, UiViewElementNode candidate) { UiElementNode parent = candidate.getUiParent(); if (parent != null) { for (UiElementNode sibling : parent.getUiChildren()) { if (sibling == beforeNode) { return false; } else if (sibling == candidate) { return true; } } } return false; } private boolean isAfter(UiViewElementNode afterNode, UiViewElementNode candidate) { UiElementNode parent = candidate.getUiParent(); if (parent != null) { for (UiElementNode sibling : parent.getUiChildren()) { if (sibling == afterNode) { return true; } else if (sibling == candidate) { return false; } } } return false; } private UiViewElementNode nextViewNode(List<ViewInfo> children, int index) { int size = children.size(); for (; index < size; index++) { ViewInfo child = children.get(index); if (child.getCookie() instanceof UiViewElementNode) { return (UiViewElementNode) child.getCookie(); } } return null; } /** Search for a subtree with valid keys and add those subtrees */ private CanvasViewInfo addKeyedSubtrees(CanvasViewInfo parent, ViewInfo viewInfo, int parentX, int parentY) { // We don't include MergeCookies when searching down for the first non-null key, // since this means we are in a "Show Included In" context, and the include tag itself // (which the merge cookie is pointing to) is still in the including-document rather // than the included document. Therefore, we only accept real UiViewElementNodes here, // not MergeCookies. if (viewInfo.getCookie() != null) { CanvasViewInfo subtree = createSubtree(parent, viewInfo, parentX, parentY); if (parent != null) { parent.mChildren.add(subtree); } return subtree; } else { for (ViewInfo child : viewInfo.getChildren()) { addKeyedSubtrees(parent, child, parentX + viewInfo.getLeft(), parentY + viewInfo.getTop()); } return null; } } } }