Home | History | Annotate | Download | only in gle2
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
     18 
     19 import static com.android.ide.common.layout.LayoutConstants.FQCN_SPACE;
     20 import static com.android.ide.common.layout.LayoutConstants.GESTURE_OVERLAY_VIEW;
     21 import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_MERGE;
     22 
     23 import com.android.ide.common.api.Margins;
     24 import com.android.ide.common.api.Rect;
     25 import com.android.ide.common.layout.GridLayoutRule;
     26 import com.android.ide.common.rendering.api.Capability;
     27 import com.android.ide.common.rendering.api.MergeCookie;
     28 import com.android.ide.common.rendering.api.ViewInfo;
     29 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
     30 import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser;
     31 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
     32 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
     33 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
     34 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     35 import com.android.util.Pair;
     36 
     37 import org.eclipse.swt.graphics.Rectangle;
     38 import org.eclipse.ui.views.properties.IPropertyDescriptor;
     39 import org.eclipse.ui.views.properties.IPropertySheetPage;
     40 import org.eclipse.ui.views.properties.IPropertySource;
     41 import org.w3c.dom.Element;
     42 import org.w3c.dom.Node;
     43 
     44 import java.util.ArrayList;
     45 import java.util.Collections;
     46 import java.util.HashMap;
     47 import java.util.LinkedList;
     48 import java.util.List;
     49 import java.util.Map;
     50 
     51 /**
     52  * Maps a {@link ViewInfo} in a structure more adapted to our needs.
     53  * The only large difference is that we keep both the original bounds of the view info
     54  * and we pre-compute the selection bounds which are absolute to the rendered image
     55  * (whereas the original bounds are relative to the parent view.)
     56  * <p/>
     57  * Each view also knows its parent and children.
     58  * <p/>
     59  * We can't alter {@link ViewInfo} as it is part of the LayoutBridge and needs to
     60  * have a fixed API.
     61  * <p/>
     62  * The view info also implements {@link IPropertySource}, which enables a linked
     63  * {@link IPropertySheetPage} to display the attributes of the selected element.
     64  * This class actually delegates handling of {@link IPropertySource} to the underlying
     65  * {@link UiViewElementNode}, if any.
     66  */
     67 public class CanvasViewInfo implements IPropertySource {
     68 
     69     /**
     70      * Minimal size of the selection, in case an empty view or layout is selected.
     71      */
     72     private static final int SELECTION_MIN_SIZE = 6;
     73 
     74 
     75     private final Rectangle mAbsRect;
     76     private final Rectangle mSelectionRect;
     77     private final String mName;
     78     private final Object mViewObject;
     79     private final UiViewElementNode mUiViewNode;
     80     private CanvasViewInfo mParent;
     81     private ViewInfo mViewInfo;
     82     private final List<CanvasViewInfo> mChildren = new ArrayList<CanvasViewInfo>();
     83 
     84     /**
     85      * Is this view info an individually exploded view? This is the case for views
     86      * that were specially inflated by the {@link UiElementPullParser} and assigned
     87      * fixed padding because they were invisible and somebody requested visibility.
     88      */
     89     private boolean mExploded;
     90 
     91     /**
     92      * Node sibling. This is usually null, but it's possible for a single node in the
     93      * model to have <b>multiple</b> separate views in the canvas, for example
     94      * when you {@code <include>} a view that has multiple widgets inside a
     95      * {@code <merge>} tag. In this case, all the views have the same node model,
     96      * the include tag, and selecting the include should highlight all the separate
     97      * views that are linked to this node. That's what this field is all about: it is
     98      * a <b>circular</b> list of all the siblings that share the same node.
     99      */
    100     private List<CanvasViewInfo> mNodeSiblings;
    101 
    102     /**
    103      * Constructs a {@link CanvasViewInfo} initialized with the given initial values.
    104      */
    105     private CanvasViewInfo(CanvasViewInfo parent, String name,
    106             Object viewObject, UiViewElementNode node, Rectangle absRect,
    107             Rectangle selectionRect, ViewInfo viewInfo) {
    108         mParent = parent;
    109         mName = name;
    110         mViewObject = viewObject;
    111         mViewInfo = viewInfo;
    112         mUiViewNode  = node;
    113         mAbsRect = absRect;
    114         mSelectionRect = selectionRect;
    115     }
    116 
    117     /**
    118      * Returns the original {@link ViewInfo} bounds in absolute coordinates
    119      * over the whole graphic.
    120      *
    121      * @return the bounding box in absolute coordinates
    122      */
    123     public Rectangle getAbsRect() {
    124         return mAbsRect;
    125     }
    126 
    127     /*
    128     * Returns the absolute selection bounds of the view info as a rectangle.
    129     * The selection bounds will always have a size greater or equal to
    130     * {@link #SELECTION_MIN_SIZE}.
    131     * The width/height is inclusive (i.e. width = right-left-1).
    132     * This is in absolute "screen" coordinates (relative to the rendered bitmap).
    133     */
    134     public Rectangle getSelectionRect() {
    135         return mSelectionRect;
    136     }
    137 
    138     /**
    139      * Returns the view node. Could be null, although unlikely.
    140      * @return An {@link UiViewElementNode} that uniquely identifies the object in the XML model.
    141      * @see ViewInfo#getCookie()
    142      */
    143     public UiViewElementNode getUiViewNode() {
    144         return mUiViewNode;
    145     }
    146 
    147     /**
    148      * Returns the parent {@link CanvasViewInfo}.
    149      * It is null for the root and non-null for children.
    150      *
    151      * @return the parent {@link CanvasViewInfo}, which can be null
    152      */
    153     public CanvasViewInfo getParent() {
    154         return mParent;
    155     }
    156 
    157     /**
    158      * Returns the list of children of this {@link CanvasViewInfo}.
    159      * The list is never null. It can be empty.
    160      * By contract, this.getChildren().get(0..n-1).getParent() == this.
    161      *
    162      * @return the children, never null
    163      */
    164     public List<CanvasViewInfo> getChildren() {
    165         return mChildren;
    166     }
    167 
    168     /**
    169      * For nodes that have multiple views rendered from a single node, such as the
    170      * children of a {@code <merge>} tag included into a separate layout, return the
    171      * "primary" view, the first view that is rendered
    172      */
    173     private CanvasViewInfo getPrimaryNodeSibling() {
    174         if (mNodeSiblings == null || mNodeSiblings.size() == 0) {
    175             return null;
    176         }
    177 
    178         return mNodeSiblings.get(0);
    179     }
    180 
    181     /**
    182      * Returns true if this view represents one view of many linked to a single node, and
    183      * where this is the primary view. The primary view is the one that will be shown
    184      * in the outline for example (since we only show nodes, not views, in the outline,
    185      * and therefore don't want repetitions when a view has more than one view info.)
    186      *
    187      * @return true if this is the primary view among more than one linked to a single
    188      *         node
    189      */
    190     private boolean isPrimaryNodeSibling() {
    191         return getPrimaryNodeSibling() == this;
    192     }
    193 
    194     /**
    195      * Returns the list of node sibling of this view (which <b>will include this
    196      * view</b>). For most views this is going to be null, but for views that share a
    197      * single node (such as widgets inside a {@code <merge>} tag included into another
    198      * layout), this will provide all the views that correspond to the node.
    199      *
    200      * @return a non-empty list of siblings (including this), or null
    201      */
    202     public List<CanvasViewInfo> getNodeSiblings() {
    203         return mNodeSiblings;
    204     }
    205 
    206     /**
    207      * Returns all the children of the canvas view info where each child corresponds to a
    208      * unique node that the user can see and select. This is intended for use by the
    209      * outline for example, where only the actual nodes are displayed, not the views
    210      * themselves.
    211      * <p>
    212      * Most views have their own nodes, so this is generally the same as
    213      * {@link #getChildren}, except in the case where you for example include a view that
    214      * has multiple widgets inside a {@code <merge>} tag, where all these widgets have the
    215      * same node (the {@code <merge>} tag).
    216      *
    217      * @return list of {@link CanvasViewInfo} objects that are children of this view,
    218      *         never null
    219      */
    220     public List<CanvasViewInfo> getUniqueChildren() {
    221         boolean haveHidden = false;
    222 
    223         for (CanvasViewInfo info : mChildren) {
    224             if (info.mNodeSiblings != null) {
    225                 // We have secondary children; must create a new collection containing
    226                 // only non-secondary children
    227                 List<CanvasViewInfo> children = new ArrayList<CanvasViewInfo>();
    228                 for (CanvasViewInfo vi : mChildren) {
    229                     if (vi.mNodeSiblings == null) {
    230                         children.add(vi);
    231                     } else if (vi.isPrimaryNodeSibling()) {
    232                         children.add(vi);
    233                     }
    234                 }
    235                 return children;
    236             }
    237 
    238             haveHidden |= info.isHidden();
    239         }
    240 
    241         if (haveHidden) {
    242             List<CanvasViewInfo> children = new ArrayList<CanvasViewInfo>(mChildren.size());
    243             for (CanvasViewInfo vi : mChildren) {
    244                 if (!vi.isHidden()) {
    245                     children.add(vi);
    246                 }
    247             }
    248 
    249             return children;
    250         }
    251 
    252         return mChildren;
    253     }
    254 
    255     /**
    256      * Returns true if the specific {@link CanvasViewInfo} is a parent
    257      * of this {@link CanvasViewInfo}. It can be a direct parent or any
    258      * grand-parent higher in the hierarchy.
    259      *
    260      * @param potentialParent the view info to check
    261      * @return true if the given info is a parent of this view
    262      */
    263     public boolean isParent(CanvasViewInfo potentialParent) {
    264         if (potentialParent == null) {
    265 
    266         }
    267         CanvasViewInfo p = mParent;
    268         while (p != null) {
    269             if (p == potentialParent) {
    270                 return true;
    271             }
    272             p = p.getParent();
    273         }
    274         return false;
    275     }
    276 
    277     /**
    278      * Returns the name of the {@link CanvasViewInfo}.
    279      * Could be null, although unlikely.
    280      * Experience shows this is the full qualified Java name of the View.
    281      * TODO: Rename this method to getFqcn.
    282      *
    283      * @return the name of the view info, or null
    284      *
    285      * @see ViewInfo#getClassName()
    286      */
    287     public String getName() {
    288         return mName;
    289     }
    290 
    291     /**
    292      * Returns the View object associated with the {@link CanvasViewInfo}.
    293      * @return the view object or null.
    294      */
    295     public Object getViewObject() {
    296         return mViewObject;
    297     }
    298 
    299     public int getBaseline() {
    300         if (mViewInfo != null) {
    301             int baseline = mViewInfo.getBaseLine();
    302             if (baseline != Integer.MIN_VALUE) {
    303                 return baseline;
    304             }
    305         }
    306 
    307         return -1;
    308     }
    309 
    310     /**
    311      * Returns the {@link Margins} for this {@link CanvasViewInfo}
    312      *
    313      * @return the {@link Margins} for this {@link CanvasViewInfo}
    314      */
    315     public Margins getMargins() {
    316         if (mViewInfo != null) {
    317             int leftMargin = mViewInfo.getLeftMargin();
    318             int topMargin = mViewInfo.getTopMargin();
    319             int rightMargin = mViewInfo.getRightMargin();
    320             int bottomMargin = mViewInfo.getBottomMargin();
    321             return new Margins(
    322                 leftMargin != Integer.MIN_VALUE ? leftMargin : 0,
    323                 rightMargin != Integer.MIN_VALUE ? rightMargin : 0,
    324                 topMargin != Integer.MIN_VALUE ? topMargin : 0,
    325                 bottomMargin != Integer.MIN_VALUE ? bottomMargin : 0
    326             );
    327         }
    328 
    329         return null;
    330     }
    331 
    332     // ---- Implementation of IPropertySource
    333 
    334     public Object getEditableValue() {
    335         UiViewElementNode uiView = getUiViewNode();
    336         if (uiView != null) {
    337             return ((IPropertySource) uiView).getEditableValue();
    338         }
    339         return null;
    340     }
    341 
    342     public IPropertyDescriptor[] getPropertyDescriptors() {
    343         UiViewElementNode uiView = getUiViewNode();
    344         if (uiView != null) {
    345             return ((IPropertySource) uiView).getPropertyDescriptors();
    346         }
    347         return null;
    348     }
    349 
    350     public Object getPropertyValue(Object id) {
    351         UiViewElementNode uiView = getUiViewNode();
    352         if (uiView != null) {
    353             return ((IPropertySource) uiView).getPropertyValue(id);
    354         }
    355         return null;
    356     }
    357 
    358     public boolean isPropertySet(Object id) {
    359         UiViewElementNode uiView = getUiViewNode();
    360         if (uiView != null) {
    361             return ((IPropertySource) uiView).isPropertySet(id);
    362         }
    363         return false;
    364     }
    365 
    366     public void resetPropertyValue(Object id) {
    367         UiViewElementNode uiView = getUiViewNode();
    368         if (uiView != null) {
    369             ((IPropertySource) uiView).resetPropertyValue(id);
    370         }
    371     }
    372 
    373     public void setPropertyValue(Object id, Object value) {
    374         UiViewElementNode uiView = getUiViewNode();
    375         if (uiView != null) {
    376             ((IPropertySource) uiView).setPropertyValue(id, value);
    377         }
    378     }
    379 
    380     /**
    381      * Returns the XML node corresponding to this info, or null if there is no
    382      * such XML node.
    383      *
    384      * @return The XML node corresponding to this info object, or null
    385      */
    386     public Node getXmlNode() {
    387         UiViewElementNode uiView = getUiViewNode();
    388         if (uiView != null) {
    389             return uiView.getXmlNode();
    390         }
    391 
    392         return null;
    393     }
    394 
    395     /**
    396      * Returns true iff this view info corresponds to a root element.
    397      *
    398      * @return True iff this is a root view info.
    399      */
    400     public boolean isRoot() {
    401         // Select the visual element -- unless it's the root.
    402         // The root element is the one whose GRAND parent
    403         // is null (because the parent will be a -document-
    404         // node).
    405 
    406         // Special case: a gesture overlay is sometimes added as the root, but for all intents
    407         // and purposes it is its layout child that is the real root so treat that one as the
    408         // root as well (such that the whole layout canvas does not highlight as part of hovers
    409         // etc)
    410         if (mParent != null
    411                 && mParent.mName.endsWith(GESTURE_OVERLAY_VIEW)
    412                 && mParent.isRoot()
    413                 && mParent.mChildren.size() == 1) {
    414             return true;
    415         }
    416 
    417         return mUiViewNode == null || mUiViewNode.getUiParent() == null ||
    418             mUiViewNode.getUiParent().getUiParent() == null;
    419     }
    420 
    421     /**
    422      * Returns true if this {@link CanvasViewInfo} represents an invisible widget that
    423      * should be highlighted when selected.  This is the case for any layout that is less than the minimum
    424      * threshold ({@link #SELECTION_MIN_SIZE}), or any other view that has -0- bounds.
    425      *
    426      * @return True if this is a tiny layout or invisible view
    427      */
    428     public boolean isInvisible() {
    429         if (isHidden()) {
    430             // Don't expand and highlight hidden widgets
    431             return false;
    432         }
    433 
    434         if (mAbsRect.width < SELECTION_MIN_SIZE || mAbsRect.height < SELECTION_MIN_SIZE) {
    435             return mUiViewNode != null && (mUiViewNode.getDescriptor().hasChildren() ||
    436                     mAbsRect.width <= 0 || mAbsRect.height <= 0);
    437         }
    438 
    439         return false;
    440     }
    441 
    442     /**
    443      * Returns true if this {@link CanvasViewInfo} represents a widget that should be
    444      * hidden, such as a {@code <Space>} which are typically not manipulated by the user
    445      * through dragging etc.
    446      *
    447      * @return true if this is a hidden view
    448      */
    449     public boolean isHidden() {
    450         if (GridLayoutRule.sDebugGridLayout) {
    451             return false;
    452         }
    453 
    454         return FQCN_SPACE.equals(mName);
    455     }
    456 
    457     /**
    458      * Is this {@link CanvasViewInfo} a view that has had its padding inflated in order to
    459      * make it visible during selection or dragging? Note that this is NOT considered to
    460      * be the case in the explode-all-views mode where all nodes have their padding
    461      * increased; it's only used for views that individually exploded because they were
    462      * requested visible and they returned true for {@link #isInvisible()}.
    463      *
    464      * @return True if this is an exploded node.
    465      */
    466     public boolean isExploded() {
    467         return mExploded;
    468     }
    469 
    470     /**
    471      * Mark this {@link CanvasViewInfo} as having been exploded or not. See the
    472      * {@link #isExploded()} method for details on what this property means.
    473      *
    474      * @param exploded New value of the exploded property to mark this info with.
    475      */
    476     /* package */ void setExploded(boolean exploded) {
    477         this.mExploded = exploded;
    478     }
    479 
    480     /**
    481      * Returns the info represented as a {@link SimpleElement}.
    482      *
    483      * @return A {@link SimpleElement} wrapping this info.
    484      */
    485     /* package */ SimpleElement toSimpleElement() {
    486 
    487         UiViewElementNode uiNode = getUiViewNode();
    488 
    489         String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor());
    490         String parentFqcn = null;
    491         Rect bounds = SwtUtils.toRect(getAbsRect());
    492         Rect parentBounds = null;
    493 
    494         UiElementNode uiParent = uiNode.getUiParent();
    495         if (uiParent != null) {
    496             parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor());
    497         }
    498         if (getParent() != null) {
    499             parentBounds = SwtUtils.toRect(getParent().getAbsRect());
    500         }
    501 
    502         SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds);
    503 
    504         for (UiAttributeNode attr : uiNode.getAllUiAttributes()) {
    505             String value = attr.getCurrentValue();
    506             if (value != null && value.length() > 0) {
    507                 AttributeDescriptor attrDesc = attr.getDescriptor();
    508                 SimpleAttribute a = new SimpleAttribute(
    509                         attrDesc.getNamespaceUri(),
    510                         attrDesc.getXmlLocalName(),
    511                         value);
    512                 e.addAttribute(a);
    513             }
    514         }
    515 
    516         for (CanvasViewInfo childVi : getChildren()) {
    517             SimpleElement e2 = childVi.toSimpleElement();
    518             if (e2 != null) {
    519                 e.addInnerElement(e2);
    520             }
    521         }
    522 
    523         return e;
    524     }
    525 
    526     /**
    527      * Returns the layout url attribute value for the closest surrounding include or
    528      * fragment element parent, or null if this {@link CanvasViewInfo} is not rendered as
    529      * part of an include or fragment tag.
    530      *
    531      * @return the layout url attribute value for the surrounding include tag, or null if
    532      *         not applicable
    533      */
    534     public String getIncludeUrl() {
    535         CanvasViewInfo curr = this;
    536         while (curr != null) {
    537             if (curr.mUiViewNode != null) {
    538                 Node node = curr.mUiViewNode.getXmlNode();
    539                 if (node != null && node.getNodeType() == Node.ELEMENT_NODE) {
    540                     String nodeName = node.getNodeName();
    541                     if (node.getNamespaceURI() == null
    542                             && LayoutDescriptors.VIEW_INCLUDE.equals(nodeName)) {
    543                         // Note: the layout attribute is NOT in the Android namespace
    544                         Element element = (Element) node;
    545                         String url = element.getAttribute(LayoutDescriptors.ATTR_LAYOUT);
    546                         if (url.length() > 0) {
    547                             return url;
    548                         }
    549                     } else if (LayoutDescriptors.VIEW_FRAGMENT.equals(nodeName)) {
    550                         String url = FragmentMenu.getFragmentLayout(node);
    551                         if (url != null) {
    552                             return url;
    553                         }
    554                     }
    555                 }
    556             }
    557             curr = curr.mParent;
    558         }
    559 
    560         return null;
    561     }
    562 
    563     /** Adds the given {@link CanvasViewInfo} as a new last child of this view */
    564     private void addChild(CanvasViewInfo child) {
    565         mChildren.add(child);
    566     }
    567 
    568     /** Adds the given {@link CanvasViewInfo} as a child at the given index */
    569     private void addChildAt(int index, CanvasViewInfo child) {
    570         mChildren.add(index, child);
    571     }
    572 
    573     /**
    574      * Removes the given {@link CanvasViewInfo} from the child list of this view, and
    575      * returns true if it was successfully removed
    576      *
    577      * @param child the child to be removed
    578      * @return true if it was a child and was removed
    579      */
    580     public boolean removeChild(CanvasViewInfo child) {
    581         return mChildren.remove(child);
    582     }
    583 
    584     @Override
    585     public String toString() {
    586         return "CanvasViewInfo [name=" + mName + ", node=" + mUiViewNode + "]";
    587     }
    588 
    589     // ---- Factory functionality ----
    590 
    591     /**
    592      * Creates a new {@link CanvasViewInfo} hierarchy based on the given {@link ViewInfo}
    593      * hierarchy. Note that this will not necessarily create one {@link CanvasViewInfo}
    594      * for each {@link ViewInfo}. It will generally only create {@link CanvasViewInfo}
    595      * objects for {@link ViewInfo} objects that contain a reference to an
    596      * {@link UiViewElementNode}, meaning that it corresponds to an element in the XML
    597      * file for this layout file. This is not always the case, such as in the following
    598      * scenarios:
    599      * <ul>
    600      * <li>we link to other layouts with {@code <include>}
    601      * <li>the current view is rendered within another view ("Show Included In") such that
    602      * the outer file does not correspond to elements in the current included XML layout
    603      * <li>on older platforms that don't support {@link Capability#EMBEDDED_LAYOUT} there
    604      * is no reference to the {@code <include>} tag
    605      * <li>with the {@code <merge>} tag we don't get a reference to the corresponding
    606      * element
    607      * <ul>
    608      * <p>
    609      * This method will build up a set of {@link CanvasViewInfo} that corresponds to the
    610      * actual <b>selectable</b> views (which are also shown in the Outline).
    611      *
    612      * @param layoutlib5 if true, the {@link ViewInfo} hierarchy was created by layoutlib
    613      *    version 5 or higher, which means this algorithm can make certain assumptions
    614      *    (for example that {@code <merge>} siblings will provide {@link MergeCookie}
    615      *    references, so we don't have to search for them.)
    616      * @param root the root {@link ViewInfo} to build from
    617      * @return a {@link CanvasViewInfo} hierarchy
    618      */
    619     public static Pair<CanvasViewInfo,List<Rectangle>> create(ViewInfo root, boolean layoutlib5) {
    620         return new Builder(layoutlib5).create(root);
    621     }
    622 
    623     /** Builder object which walks over a tree of {@link ViewInfo} objects and builds
    624      * up a corresponding {@link CanvasViewInfo} hierarchy. */
    625     private static class Builder {
    626         public Builder(boolean layoutlib5) {
    627             mLayoutLib5 = layoutlib5;
    628         }
    629 
    630         /**
    631          * The mapping from nodes that have a {@code <merge>} as a parent in the node
    632          * model to their corresponding views
    633          */
    634         private Map<UiViewElementNode, List<CanvasViewInfo>> mMergeNodeMap;
    635 
    636         /**
    637          * Whether the ViewInfos are provided by a layout library that is version 5 or
    638          * later, since that will allow us to take several shortcuts
    639          */
    640         private boolean mLayoutLib5;
    641 
    642         /**
    643          * Creates a hierarchy of {@link CanvasViewInfo} objects and merge bounding
    644          * rectangles from the given {@link ViewInfo} hierarchy
    645          */
    646         private Pair<CanvasViewInfo,List<Rectangle>> create(ViewInfo root) {
    647             Object cookie = root.getCookie();
    648             if (cookie == null) {
    649                 // Special case: If the root-most view does not have a view cookie,
    650                 // then we are rendering some outer layout surrounding this layout, and in
    651                 // that case we must search down the hierarchy for the (possibly multiple)
    652                 // sub-roots that correspond to elements in this layout, and place them inside
    653                 // an outer view that has no node. In the outline this item will be used to
    654                 // show the inclusion-context.
    655                 CanvasViewInfo rootView = createView(null, root, 0, 0);
    656                 addKeyedSubtrees(rootView, root, 0, 0);
    657 
    658                 List<Rectangle> includedBounds = new ArrayList<Rectangle>();
    659                 for (CanvasViewInfo vi : rootView.getChildren()) {
    660                     if (vi.getNodeSiblings() == null || vi.isPrimaryNodeSibling()) {
    661                         includedBounds.add(vi.getAbsRect());
    662                     }
    663                 }
    664 
    665                 // There are <merge> nodes here; see if we can insert it into the hierarchy
    666                 if (mMergeNodeMap != null) {
    667                     // Locate all the nodes that have a <merge> as a parent in the node model,
    668                     // and where the view sits at the top level inside the include-context node.
    669                     UiViewElementNode merge = null;
    670                     List<CanvasViewInfo> merged = new ArrayList<CanvasViewInfo>();
    671                     for (Map.Entry<UiViewElementNode, List<CanvasViewInfo>> entry : mMergeNodeMap
    672                             .entrySet()) {
    673                         UiViewElementNode node = entry.getKey();
    674                         if (!hasMergeParent(node)) {
    675                             continue;
    676                         }
    677                         List<CanvasViewInfo> views = entry.getValue();
    678                         assert views.size() > 0;
    679                         CanvasViewInfo view = views.get(0); // primary
    680                         if (view.getParent() != rootView) {
    681                             continue;
    682                         }
    683                         UiElementNode parent = node.getUiParent();
    684                         if (merge != null && parent != merge) {
    685                             continue;
    686                         }
    687                         merge = (UiViewElementNode) parent;
    688                         merged.add(view);
    689                     }
    690                     if (merged.size() > 0) {
    691                         // Compute a bounding box for the merged views
    692                         Rectangle absRect = null;
    693                         for (CanvasViewInfo child : merged) {
    694                             Rectangle rect = child.getAbsRect();
    695                             if (absRect == null) {
    696                                 absRect = rect;
    697                             } else {
    698                                 absRect = absRect.union(rect);
    699                             }
    700                         }
    701 
    702                         CanvasViewInfo mergeView = new CanvasViewInfo(rootView, VIEW_MERGE, null,
    703                                 merge, absRect, absRect, null /* viewInfo */);
    704                         for (CanvasViewInfo view : merged) {
    705                             if (rootView.removeChild(view)) {
    706                                 mergeView.addChild(view);
    707                             }
    708                         }
    709                         rootView.addChild(mergeView);
    710                     }
    711                 }
    712 
    713                 return Pair.of(rootView, includedBounds);
    714             } else {
    715                 // We have a view key at the top, so just go and create {@link CanvasViewInfo}
    716                 // objects for each {@link ViewInfo} until we run into a null key.
    717                 CanvasViewInfo rootView = addKeyedSubtrees(null, root, 0, 0);
    718 
    719                 // Special case: look to see if the root element is really a <merge>, and if so,
    720                 // manufacture a view for it such that we can target this root element
    721                 // in drag & drop operations, such that we can show it in the outline, etc
    722                 if (rootView != null && hasMergeParent(rootView.getUiViewNode())) {
    723                     CanvasViewInfo merge = new CanvasViewInfo(null, VIEW_MERGE, null,
    724                             (UiViewElementNode) rootView.getUiViewNode().getUiParent(),
    725                             rootView.getAbsRect(), rootView.getSelectionRect(),
    726                             null /* viewInfo */);
    727                     // Insert the <merge> as the new real root
    728                     rootView.mParent = merge;
    729                     merge.addChild(rootView);
    730                     rootView = merge;
    731                 }
    732 
    733                 return Pair.of(rootView, null);
    734             }
    735         }
    736 
    737         private boolean hasMergeParent(UiViewElementNode rootNode) {
    738             UiElementNode rootParent = rootNode.getUiParent();
    739             return (rootParent instanceof UiViewElementNode
    740                     && VIEW_MERGE.equals(rootParent.getDescriptor().getXmlName()));
    741         }
    742 
    743         /** Creates a {@link CanvasViewInfo} for a given {@link ViewInfo} but does not recurse */
    744         private CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX,
    745                 int parentY) {
    746             Object cookie = root.getCookie();
    747             UiViewElementNode node = null;
    748             if (cookie instanceof UiViewElementNode) {
    749                 node = (UiViewElementNode) cookie;
    750             } else if (cookie instanceof MergeCookie) {
    751                 cookie = ((MergeCookie) cookie).getCookie();
    752                 if (cookie instanceof UiViewElementNode) {
    753                     node = (UiViewElementNode) cookie;
    754                     CanvasViewInfo view = createView(parent, root, parentX, parentY, node);
    755                     if (root.getCookie() instanceof MergeCookie && view.mNodeSiblings == null) {
    756                         List<CanvasViewInfo> v = mMergeNodeMap == null ?
    757                                 null : mMergeNodeMap.get(node);
    758                         if (v != null) {
    759                             v.add(view);
    760                         } else {
    761                             v = new ArrayList<CanvasViewInfo>();
    762                             v.add(view);
    763                             if (mMergeNodeMap == null) {
    764                                 mMergeNodeMap =
    765                                     new HashMap<UiViewElementNode, List<CanvasViewInfo>>();
    766                             }
    767                             mMergeNodeMap.put(node, v);
    768                         }
    769                         view.mNodeSiblings = v;
    770                     }
    771 
    772                     return view;
    773                 }
    774             }
    775 
    776             return createView(parent, root, parentX, parentY, node);
    777         }
    778 
    779         /**
    780          * Creates a {@link CanvasViewInfo} for a given {@link ViewInfo} but does not recurse.
    781          * This method specifies an explicit {@link UiViewElementNode} to use rather than
    782          * relying on the view cookie in the info object.
    783          */
    784         private CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX,
    785                 int parentY, UiViewElementNode node) {
    786 
    787             int x = root.getLeft();
    788             int y = root.getTop();
    789             int w = root.getRight() - x;
    790             int h = root.getBottom() - y;
    791 
    792             x += parentX;
    793             y += parentY;
    794 
    795             Rectangle absRect = new Rectangle(x, y, w - 1, h - 1);
    796 
    797             if (w < SELECTION_MIN_SIZE) {
    798                 int d = (SELECTION_MIN_SIZE - w) / 2;
    799                 x -= d;
    800                 w += SELECTION_MIN_SIZE - w;
    801             }
    802 
    803             if (h < SELECTION_MIN_SIZE) {
    804                 int d = (SELECTION_MIN_SIZE - h) / 2;
    805                 y -= d;
    806                 h += SELECTION_MIN_SIZE - h;
    807             }
    808 
    809             Rectangle selectionRect = new Rectangle(x, y, w - 1, h - 1);
    810 
    811             return new CanvasViewInfo(parent, root.getClassName(), root.getViewObject(), node,
    812                     absRect, selectionRect, root);
    813         }
    814 
    815         /** Create a subtree recursively until you run out of keys */
    816         private CanvasViewInfo createSubtree(CanvasViewInfo parent, ViewInfo viewInfo,
    817                 int parentX, int parentY) {
    818             assert viewInfo.getCookie() != null;
    819 
    820             CanvasViewInfo view = createView(parent, viewInfo, parentX, parentY);
    821             // Bug workaround: Ensure that we never have a child node identical
    822             // to its parent node: this can happen for example when rendering a
    823             // ZoomControls view where the merge cookies point to the parent.
    824             if (parent != null && view.mUiViewNode == parent.mUiViewNode) {
    825                 return null;
    826             }
    827 
    828             // Process children:
    829             parentX += viewInfo.getLeft();
    830             parentY += viewInfo.getTop();
    831 
    832             List<ViewInfo> children = viewInfo.getChildren();
    833 
    834             if (mLayoutLib5) {
    835                 for (ViewInfo child : children) {
    836                     Object cookie = child.getCookie();
    837                     if (cookie instanceof UiViewElementNode || cookie instanceof MergeCookie) {
    838                         CanvasViewInfo childView = createSubtree(view, child,
    839                                 parentX, parentY);
    840                         if (childView != null) {
    841                             view.addChild(childView);
    842                         }
    843                     } // else: null cookies, adapter item references, etc: No child views.
    844                 }
    845 
    846                 return view;
    847             }
    848 
    849             // See if we have any missing keys at this level
    850             int missingNodes = 0;
    851             int mergeNodes = 0;
    852             for (ViewInfo child : children) {
    853                 // Only use children which have a ViewKey of the correct type.
    854                 // We can't interact with those when they have a null key or
    855                 // an incompatible type.
    856                 Object cookie = child.getCookie();
    857                 if (!(cookie instanceof UiViewElementNode)) {
    858                     if (cookie instanceof MergeCookie) {
    859                         mergeNodes++;
    860                     } else {
    861                         missingNodes++;
    862                     }
    863                 }
    864             }
    865 
    866             if (missingNodes == 0 && mergeNodes == 0) {
    867                 // No missing nodes; this is the normal case, and we can just continue to
    868                 // recursively add our children
    869                 for (ViewInfo child : children) {
    870                     CanvasViewInfo childView = createSubtree(view, child,
    871                             parentX, parentY);
    872                     view.addChild(childView);
    873                 }
    874 
    875                 // TBD: Emit placeholder views for keys that have no views?
    876             } else {
    877                 // We don't have keys for one or more of the ViewInfos. There are many
    878                 // possible causes: we are on an SDK platform that does not support
    879                 // embedded_layout rendering, or we are including a view with a <merge>
    880                 // as the root element.
    881 
    882                 UiViewElementNode uiViewNode = view.getUiViewNode();
    883                 String containerName = uiViewNode != null
    884                     ? uiViewNode.getDescriptor().getXmlLocalName() : ""; //$NON-NLS-1$
    885                 if (containerName.equals(LayoutDescriptors.VIEW_INCLUDE)) {
    886                     // This is expected -- we don't WANT to get node keys for the content
    887                     // of an include since it's in a different file and should be treated
    888                     // as a single unit that cannot be edited (hence, no CanvasViewInfo
    889                     // children)
    890                 } else {
    891                     // We are getting children with null keys where we don't expect it;
    892                     // this usually means that we are dealing with an Android platform
    893                     // that does not support {@link Capability#EMBEDDED_LAYOUT}, or
    894                     // that there are <merge> tags which are doing surprising things
    895                     // to the view hierarchy
    896                     LinkedList<UiViewElementNode> unused = new LinkedList<UiViewElementNode>();
    897                     if (uiViewNode != null) {
    898                         for (UiElementNode child : uiViewNode.getUiChildren()) {
    899                             if (child instanceof UiViewElementNode) {
    900                                 unused.addLast((UiViewElementNode) child);
    901                             }
    902                         }
    903                     }
    904                     for (ViewInfo child : children) {
    905                         Object cookie = child.getCookie();
    906                         if (mergeNodes > 0 && cookie instanceof MergeCookie) {
    907                             cookie = ((MergeCookie) cookie).getCookie();
    908                         }
    909                         if (cookie != null) {
    910                             unused.remove(cookie);
    911                         }
    912                     }
    913 
    914                     if (unused.size() > 0 || mergeNodes > 0) {
    915                         if (unused.size() == missingNodes) {
    916                             // The number of unmatched elements and ViewInfos are identical;
    917                             // it's very likely that they match one to one, so just use these
    918                             for (ViewInfo child : children) {
    919                                 if (child.getCookie() == null) {
    920                                     // Only create a flat (non-recursive) view
    921                                     CanvasViewInfo childView = createView(view, child, parentX,
    922                                             parentY, unused.removeFirst());
    923                                     view.addChild(childView);
    924                                 } else {
    925                                     CanvasViewInfo childView = createSubtree(view, child, parentX,
    926                                             parentY);
    927                                     view.addChild(childView);
    928                                 }
    929                             }
    930                         } else {
    931                             // We have an uneven match. In this case we might be dealing
    932                             // with <merge> etc.
    933                             // We have no way to associate elements back with the
    934                             // corresponding <include> tags if there are more than one of
    935                             // them. That's not a huge tragedy since visually you are not
    936                             // allowed to edit these anyway; we just need to make a visual
    937                             // block for these for selection and outline purposes.
    938                             addMismatched(view, parentX, parentY, children, unused);
    939                         }
    940                     } else {
    941                         // No unused keys, but there are views without keys.
    942                         // We can't represent these since all views must have node keys
    943                         // such that you can operate on them. Just ignore these.
    944                         for (ViewInfo child : children) {
    945                             if (child.getCookie() != null) {
    946                                 CanvasViewInfo childView = createSubtree(view, child,
    947                                         parentX, parentY);
    948                                 view.addChild(childView);
    949                             }
    950                         }
    951                     }
    952                 }
    953             }
    954 
    955             return view;
    956         }
    957 
    958         /**
    959          * We have various {@link ViewInfo} children with null keys, and/or nodes in
    960          * the corresponding UI model that are not referenced by any of the {@link ViewInfo}
    961          * objects. This method attempts to account for this, by matching the views in
    962          * the right order.
    963          */
    964         private void addMismatched(CanvasViewInfo parentView, int parentX, int parentY,
    965                 List<ViewInfo> children, LinkedList<UiViewElementNode> unused) {
    966             UiViewElementNode afterNode = null;
    967             UiViewElementNode beforeNode = null;
    968             // We have one important clue we can use when matching unused nodes
    969             // with views: if we have a view V1 with node N1, and a view V2 with node N2,
    970             // then we can only match unknown node UN with unknown node UV if
    971             // V1 < UV < V2 and N1 < UN < N2.
    972             // We can use these constraints to do the matching, for example by
    973             // a simple DAG traversal. However, since the number of unmatched nodes
    974             // will typically be very small, we'll just do a simple algorithm here
    975             // which checks forwards/backwards whether a match is valid.
    976             for (int index = 0, size = children.size(); index < size; index++) {
    977                 ViewInfo child = children.get(index);
    978                 if (child.getCookie() != null) {
    979                     CanvasViewInfo childView = createSubtree(parentView, child, parentX, parentY);
    980                     if (childView != null) {
    981                         parentView.addChild(childView);
    982                     }
    983                     if (child.getCookie() instanceof UiViewElementNode) {
    984                         afterNode = (UiViewElementNode) child.getCookie();
    985                     }
    986                 } else {
    987                     beforeNode = nextViewNode(children, index);
    988 
    989                     // Find first eligible node from unused
    990                     // TOD: What if there are more eligible? We need to process ALL views
    991                     // and all nodes in one go here
    992 
    993                     UiViewElementNode matching = null;
    994                     for (UiViewElementNode candidate : unused) {
    995                         if (afterNode == null || isAfter(afterNode, candidate)) {
    996                             if (beforeNode == null || isBefore(beforeNode, candidate)) {
    997                                 matching = candidate;
    998                                 break;
    999                             }
   1000                         }
   1001                     }
   1002 
   1003                     if (matching != null) {
   1004                         unused.remove(matching);
   1005                         CanvasViewInfo childView = createView(parentView, child, parentX, parentY,
   1006                                 matching);
   1007                         parentView.addChild(childView);
   1008                         afterNode = matching;
   1009                     } else {
   1010                         // We have no node for the view -- what do we do??
   1011                         // Nothing - we only represent stuff in the outline that is in the
   1012                         // source model, not in the render
   1013                     }
   1014                 }
   1015             }
   1016 
   1017             // Add zero-bounded boxes for all remaining nodes since they need to show
   1018             // up in the outline, need to be selectable so you can press Delete, etc.
   1019             if (unused.size() > 0) {
   1020                 Map<UiViewElementNode, Integer> rankMap =
   1021                     new HashMap<UiViewElementNode, Integer>();
   1022                 Map<UiViewElementNode, CanvasViewInfo> infoMap =
   1023                     new HashMap<UiViewElementNode, CanvasViewInfo>();
   1024                 UiElementNode parent = unused.get(0).getUiParent();
   1025                 if (parent != null) {
   1026                     int index = 0;
   1027                     for (UiElementNode child : parent.getUiChildren()) {
   1028                         UiViewElementNode node = (UiViewElementNode) child;
   1029                         rankMap.put(node, index++);
   1030                     }
   1031                     for (CanvasViewInfo child : parentView.getChildren()) {
   1032                         infoMap.put(child.getUiViewNode(), child);
   1033                     }
   1034                     List<Integer> usedIndexes = new ArrayList<Integer>();
   1035                     for (UiViewElementNode node : unused) {
   1036                         Integer rank = rankMap.get(node);
   1037                         if (rank != null) {
   1038                             usedIndexes.add(rank);
   1039                         }
   1040                     }
   1041                     Collections.sort(usedIndexes);
   1042                     for (int i = usedIndexes.size() - 1; i >= 0; i--) {
   1043                         Integer rank = usedIndexes.get(i);
   1044                         UiViewElementNode found = null;
   1045                         for (UiViewElementNode node : unused) {
   1046                             if (rankMap.get(node) == rank) {
   1047                                 found = node;
   1048                                 break;
   1049                             }
   1050                         }
   1051                         if (found != null) {
   1052                             Rectangle absRect = new Rectangle(parentX, parentY, 0, 0);
   1053                             String name = found.getDescriptor().getXmlLocalName();
   1054                             CanvasViewInfo v = new CanvasViewInfo(parentView, name, null, found,
   1055                                     absRect, absRect, null /* viewInfo */);
   1056                             // Find corresponding index in the parent view
   1057                             List<CanvasViewInfo> siblings = parentView.getChildren();
   1058                             int insertPosition = siblings.size();
   1059                             for (int j = siblings.size() - 1; j >= 0; j--) {
   1060                                 CanvasViewInfo sibling = siblings.get(j);
   1061                                 UiViewElementNode siblingNode = sibling.getUiViewNode();
   1062                                 if (siblingNode != null) {
   1063                                     Integer siblingRank = rankMap.get(siblingNode);
   1064                                     if (siblingRank != null && siblingRank < rank) {
   1065                                         insertPosition = j + 1;
   1066                                         break;
   1067                                     }
   1068                                 }
   1069                             }
   1070                             parentView.addChildAt(insertPosition, v);
   1071                             unused.remove(found);
   1072                         }
   1073                     }
   1074                 }
   1075                 // Add in any remaining
   1076                 for (UiViewElementNode node : unused) {
   1077                     Rectangle absRect = new Rectangle(parentX, parentY, 0, 0);
   1078                     String name = node.getDescriptor().getXmlLocalName();
   1079                     CanvasViewInfo v = new CanvasViewInfo(parentView, name, null, node, absRect,
   1080                             absRect, null /* viewInfo */);
   1081                     parentView.addChild(v);
   1082                 }
   1083             }
   1084         }
   1085 
   1086         private boolean isBefore(UiViewElementNode beforeNode, UiViewElementNode candidate) {
   1087             UiElementNode parent = candidate.getUiParent();
   1088             if (parent != null) {
   1089                 for (UiElementNode sibling : parent.getUiChildren()) {
   1090                     if (sibling == beforeNode) {
   1091                         return false;
   1092                     } else if (sibling == candidate) {
   1093                         return true;
   1094                     }
   1095                 }
   1096             }
   1097             return false;
   1098         }
   1099 
   1100         private boolean isAfter(UiViewElementNode afterNode, UiViewElementNode candidate) {
   1101             UiElementNode parent = candidate.getUiParent();
   1102             if (parent != null) {
   1103                 for (UiElementNode sibling : parent.getUiChildren()) {
   1104                     if (sibling == afterNode) {
   1105                         return true;
   1106                     } else if (sibling == candidate) {
   1107                         return false;
   1108                     }
   1109                 }
   1110             }
   1111             return false;
   1112         }
   1113 
   1114         private UiViewElementNode nextViewNode(List<ViewInfo> children, int index) {
   1115             int size = children.size();
   1116             for (; index < size; index++) {
   1117                 ViewInfo child = children.get(index);
   1118                 if (child.getCookie() instanceof UiViewElementNode) {
   1119                     return (UiViewElementNode) child.getCookie();
   1120                 }
   1121             }
   1122 
   1123             return null;
   1124         }
   1125 
   1126         /** Search for a subtree with valid keys and add those subtrees */
   1127         private CanvasViewInfo addKeyedSubtrees(CanvasViewInfo parent, ViewInfo viewInfo,
   1128                 int parentX, int parentY) {
   1129             // We don't include MergeCookies when searching down for the first non-null key,
   1130             // since this means we are in a "Show Included In" context, and the include tag itself
   1131             // (which the merge cookie is pointing to) is still in the including-document rather
   1132             // than the included document. Therefore, we only accept real UiViewElementNodes here,
   1133             // not MergeCookies.
   1134             if (viewInfo.getCookie() != null) {
   1135                 CanvasViewInfo subtree = createSubtree(parent, viewInfo, parentX, parentY);
   1136                 if (parent != null && subtree != null) {
   1137                     parent.mChildren.add(subtree);
   1138                 }
   1139                 return subtree;
   1140             } else {
   1141                 for (ViewInfo child : viewInfo.getChildren()) {
   1142                     addKeyedSubtrees(parent, child, parentX + viewInfo.getLeft(), parentY
   1143                             + viewInfo.getTop());
   1144                 }
   1145 
   1146                 return null;
   1147             }
   1148         }
   1149     }
   1150 }
   1151