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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 295 public Object getViewObject() { 296 return mViewObject; 297 } 298 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 */ 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 334 public Object getEditableValue() { 335 UiViewElementNode uiView = getUiViewNode(); 336 if (uiView != null) { 337 return ((IPropertySource) uiView).getEditableValue(); 338 } 339 return null; 340 } 341 342 public IPropertyDescriptor[] getPropertyDescriptors() { 343 UiViewElementNode uiView = getUiViewNode(); 344 if (uiView != null) { 345 return ((IPropertySource) uiView).getPropertyDescriptors(); 346 } 347 return null; 348 } 349 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 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 366 public void resetPropertyValue(Object id) { 367 UiViewElementNode uiView = getUiViewNode(); 368 if (uiView != null) { 369 ((IPropertySource) uiView).resetPropertyValue(id); 370 } 371 } 372 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 580 public boolean removeChild(CanvasViewInfo child) { 581 return mChildren.remove(child); 582 } 583 584 @Override 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 */ 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 { 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 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 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 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 */ 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