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