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