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