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