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      */
CanvasViewInfo(CanvasViewInfo parent, String name, Object viewObject, UiViewElementNode node, Rectangle absRect, Rectangle selectionRect, ViewInfo viewInfo)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
getAbsRect()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
getSelectionRect()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
getUiViewNode()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
getParent()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
getChildren()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
getPrimaryNodeSibling()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      */
isPrimaryNodeSibling()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
getNodeSiblings()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
getUniqueChildren()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      */
isParent(@onNull CanvasViewInfo potentialParent)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
getName()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
getViewObject()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      */
getBaseline()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
getMargins()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
getEditableValue()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
getPropertyDescriptors()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
getPropertyValue(Object id)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
isPropertySet(Object id)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
resetPropertyValue(Object id)389     public void resetPropertyValue(Object id) {
390         UiViewElementNode uiView = getUiViewNode();
391         if (uiView != null) {
392             ((IPropertySource) uiView).resetPropertyValue(id);
393         }
394     }
395 
396     @Override
setPropertyValue(Object id, Object value)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
getXmlNode()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      */
isRoot()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      */
isInvisible()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      */
isHidden()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      */
isExploded()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      */
setExploded(boolean exploded)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
toSimpleElement()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
getIncludeUrl()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 */
addChild(@onNull CanvasViewInfo child)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 */
addChildAt(int index, @NonNull CanvasViewInfo child)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      */
removeChild(@onNull CanvasViewInfo child)607     public boolean removeChild(@NonNull CanvasViewInfo child) {
608         return mChildren.remove(child);
609     }
610 
611     @Override
toString()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
create(ViewInfo root, boolean layoutlib5)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 {
Builder(boolean layoutlib5)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          */
create(ViewInfo root)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 
hasMergeParent(UiViewElementNode rootNode)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 */
createView(CanvasViewInfo parent, ViewInfo root, int parentX, int parentY)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          */
createView(CanvasViewInfo parent, ViewInfo root, int parentX, int parentY, UiViewElementNode node)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 */
createSubtree(CanvasViewInfo parent, ViewInfo viewInfo, int parentX, int parentY)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          */
addMismatched(CanvasViewInfo parentView, int parentX, int parentY, List<ViewInfo> children, LinkedList<UiViewElementNode> unused)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 
isBefore(UiViewElementNode beforeNode, UiViewElementNode candidate)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 
isAfter(UiViewElementNode afterNode, UiViewElementNode candidate)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 
nextViewNode(List<ViewInfo> children, int index)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 */
addKeyedSubtrees(CanvasViewInfo parent, ViewInfo viewInfo, int parentX, int parentY)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