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