| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Eclipse Public License, Version 1.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.eclipse.org/org/documents/epl-v10.php |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.ide.eclipse.adt.internal.editors.layout.gle2; |
| |
| import static com.android.SdkConstants.FQCN_SPACE; |
| import static com.android.SdkConstants.FQCN_SPACE_V7; |
| import static com.android.SdkConstants.GESTURE_OVERLAY_VIEW; |
| import static com.android.SdkConstants.VIEW_MERGE; |
| |
| import com.android.SdkConstants; |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.ide.common.api.Margins; |
| import com.android.ide.common.api.Rect; |
| import com.android.ide.common.layout.GridLayoutRule; |
| import com.android.ide.common.rendering.api.Capability; |
| import com.android.ide.common.rendering.api.MergeCookie; |
| import com.android.ide.common.rendering.api.ViewInfo; |
| import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; |
| import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser; |
| import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; |
| import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; |
| import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; |
| import com.android.utils.Pair; |
| |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.ui.views.properties.IPropertyDescriptor; |
| import org.eclipse.ui.views.properties.IPropertySheetPage; |
| import org.eclipse.ui.views.properties.IPropertySource; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Maps a {@link ViewInfo} in a structure more adapted to our needs. |
| * The only large difference is that we keep both the original bounds of the view info |
| * and we pre-compute the selection bounds which are absolute to the rendered image |
| * (whereas the original bounds are relative to the parent view.) |
| * <p/> |
| * Each view also knows its parent and children. |
| * <p/> |
| * We can't alter {@link ViewInfo} as it is part of the LayoutBridge and needs to |
| * have a fixed API. |
| * <p/> |
| * The view info also implements {@link IPropertySource}, which enables a linked |
| * {@link IPropertySheetPage} to display the attributes of the selected element. |
| * This class actually delegates handling of {@link IPropertySource} to the underlying |
| * {@link UiViewElementNode}, if any. |
| */ |
| public class CanvasViewInfo implements IPropertySource { |
| |
| /** |
| * Minimal size of the selection, in case an empty view or layout is selected. |
| */ |
| public static final int SELECTION_MIN_SIZE = 6; |
| |
| private final Rectangle mAbsRect; |
| private final Rectangle mSelectionRect; |
| private final String mName; |
| private final Object mViewObject; |
| private final UiViewElementNode mUiViewNode; |
| private CanvasViewInfo mParent; |
| private ViewInfo mViewInfo; |
| private final List<CanvasViewInfo> mChildren = new ArrayList<CanvasViewInfo>(); |
| |
| /** |
| * Is this view info an individually exploded view? This is the case for views |
| * that were specially inflated by the {@link UiElementPullParser} and assigned |
| * fixed padding because they were invisible and somebody requested visibility. |
| */ |
| private boolean mExploded; |
| |
| /** |
| * Node sibling. This is usually null, but it's possible for a single node in the |
| * model to have <b>multiple</b> separate views in the canvas, for example |
| * when you {@code <include>} a view that has multiple widgets inside a |
| * {@code <merge>} tag. In this case, all the views have the same node model, |
| * the include tag, and selecting the include should highlight all the separate |
| * views that are linked to this node. That's what this field is all about: it is |
| * a <b>circular</b> list of all the siblings that share the same node. |
| */ |
| private List<CanvasViewInfo> mNodeSiblings; |
| |
| /** |
| * Constructs a {@link CanvasViewInfo} initialized with the given initial values. |
| */ |
| private CanvasViewInfo(CanvasViewInfo parent, String name, |
| Object viewObject, UiViewElementNode node, Rectangle absRect, |
| Rectangle selectionRect, ViewInfo viewInfo) { |
| mParent = parent; |
| mName = name; |
| mViewObject = viewObject; |
| mViewInfo = viewInfo; |
| mUiViewNode = node; |
| mAbsRect = absRect; |
| mSelectionRect = selectionRect; |
| } |
| |
| /** |
| * Returns the original {@link ViewInfo} bounds in absolute coordinates |
| * over the whole graphic. |
| * |
| * @return the bounding box in absolute coordinates |
| */ |
| @NonNull |
| public Rectangle getAbsRect() { |
| return mAbsRect; |
| } |
| |
| /** |
| * Returns the absolute selection bounds of the view info as a rectangle. |
| * The selection bounds will always have a size greater or equal to |
| * {@link #SELECTION_MIN_SIZE}. |
| * The width/height is inclusive (i.e. width = right-left-1). |
| * This is in absolute "screen" coordinates (relative to the rendered bitmap). |
| * |
| * @return the absolute selection bounds |
| */ |
| @NonNull |
| public Rectangle getSelectionRect() { |
| return mSelectionRect; |
| } |
| |
| /** |
| * Returns the view node. Could be null, although unlikely. |
| * @return An {@link UiViewElementNode} that uniquely identifies the object in the XML model. |
| * @see ViewInfo#getCookie() |
| */ |
| @Nullable |
| public UiViewElementNode getUiViewNode() { |
| return mUiViewNode; |
| } |
| |
| /** |
| * Returns the parent {@link CanvasViewInfo}. |
| * It is null for the root and non-null for children. |
| * |
| * @return the parent {@link CanvasViewInfo}, which can be null |
| */ |
| @Nullable |
| public CanvasViewInfo getParent() { |
| return mParent; |
| } |
| |
| /** |
| * Returns the list of children of this {@link CanvasViewInfo}. |
| * The list is never null. It can be empty. |
| * By contract, this.getChildren().get(0..n-1).getParent() == this. |
| * |
| * @return the children, never null |
| */ |
| @NonNull |
| public List<CanvasViewInfo> getChildren() { |
| return mChildren; |
| } |
| |
| /** |
| * For nodes that have multiple views rendered from a single node, such as the |
| * children of a {@code <merge>} tag included into a separate layout, return the |
| * "primary" view, the first view that is rendered |
| */ |
| @Nullable |
| private CanvasViewInfo getPrimaryNodeSibling() { |
| if (mNodeSiblings == null || mNodeSiblings.size() == 0) { |
| return null; |
| } |
| |
| return mNodeSiblings.get(0); |
| } |
| |
| /** |
| * Returns true if this view represents one view of many linked to a single node, and |
| * where this is the primary view. The primary view is the one that will be shown |
| * in the outline for example (since we only show nodes, not views, in the outline, |
| * and therefore don't want repetitions when a view has more than one view info.) |
| * |
| * @return true if this is the primary view among more than one linked to a single |
| * node |
| */ |
| private boolean isPrimaryNodeSibling() { |
| return getPrimaryNodeSibling() == this; |
| } |
| |
| /** |
| * Returns the list of node sibling of this view (which <b>will include this |
| * view</b>). For most views this is going to be null, but for views that share a |
| * single node (such as widgets inside a {@code <merge>} tag included into another |
| * layout), this will provide all the views that correspond to the node. |
| * |
| * @return a non-empty list of siblings (including this), or null |
| */ |
| @Nullable |
| public List<CanvasViewInfo> getNodeSiblings() { |
| return mNodeSiblings; |
| } |
| |
| /** |
| * Returns all the children of the canvas view info where each child corresponds to a |
| * unique node that the user can see and select. This is intended for use by the |
| * outline for example, where only the actual nodes are displayed, not the views |
| * themselves. |
| * <p> |
| * Most views have their own nodes, so this is generally the same as |
| * {@link #getChildren}, except in the case where you for example include a view that |
| * has multiple widgets inside a {@code <merge>} tag, where all these widgets have the |
| * same node (the {@code <merge>} tag). |
| * |
| * @return list of {@link CanvasViewInfo} objects that are children of this view, |
| * never null |
| */ |
| @NonNull |
| public List<CanvasViewInfo> getUniqueChildren() { |
| boolean haveHidden = false; |
| |
| for (CanvasViewInfo info : mChildren) { |
| if (info.mNodeSiblings != null) { |
| // We have secondary children; must create a new collection containing |
| // only non-secondary children |
| List<CanvasViewInfo> children = new ArrayList<CanvasViewInfo>(); |
| for (CanvasViewInfo vi : mChildren) { |
| if (vi.mNodeSiblings == null) { |
| children.add(vi); |
| } else if (vi.isPrimaryNodeSibling()) { |
| children.add(vi); |
| } |
| } |
| return children; |
| } |
| |
| haveHidden |= info.isHidden(); |
| } |
| |
| if (haveHidden) { |
| List<CanvasViewInfo> children = new ArrayList<CanvasViewInfo>(mChildren.size()); |
| for (CanvasViewInfo vi : mChildren) { |
| if (!vi.isHidden()) { |
| children.add(vi); |
| } |
| } |
| |
| return children; |
| } |
| |
| return mChildren; |
| } |
| |
| /** |
| * Returns true if the specific {@link CanvasViewInfo} is a parent |
| * of this {@link CanvasViewInfo}. It can be a direct parent or any |
| * grand-parent higher in the hierarchy. |
| * |
| * @param potentialParent the view info to check |
| * @return true if the given info is a parent of this view |
| */ |
| public boolean isParent(@NonNull CanvasViewInfo potentialParent) { |
| CanvasViewInfo p = mParent; |
| while (p != null) { |
| if (p == potentialParent) { |
| return true; |
| } |
| p = p.getParent(); |
| } |
| return false; |
| } |
| |
| /** |
| * Returns the name of the {@link CanvasViewInfo}. |
| * Could be null, although unlikely. |
| * Experience shows this is the full qualified Java name of the View. |
| * TODO: Rename this method to getFqcn. |
| * |
| * @return the name of the view info |
| * |
| * @see ViewInfo#getClassName() |
| */ |
| @NonNull |
| public String getName() { |
| return mName; |
| } |
| |
| /** |
| * Returns the View object associated with the {@link CanvasViewInfo}. |
| * @return the view object or null. |
| */ |
| @Nullable |
| public Object getViewObject() { |
| return mViewObject; |
| } |
| |
| /** |
| * Returns the baseline of this object, or -1 if it does not support a baseline |
| * |
| * @return the baseline or -1 |
| */ |
| public int getBaseline() { |
| if (mViewInfo != null) { |
| int baseline = mViewInfo.getBaseLine(); |
| if (baseline != Integer.MIN_VALUE) { |
| return baseline; |
| } |
| } |
| |
| return -1; |
| } |
| |
| /** |
| * Returns the {@link Margins} for this {@link CanvasViewInfo} |
| * |
| * @return the {@link Margins} for this {@link CanvasViewInfo} |
| */ |
| @Nullable |
| public Margins getMargins() { |
| if (mViewInfo != null) { |
| int leftMargin = mViewInfo.getLeftMargin(); |
| int topMargin = mViewInfo.getTopMargin(); |
| int rightMargin = mViewInfo.getRightMargin(); |
| int bottomMargin = mViewInfo.getBottomMargin(); |
| return new Margins( |
| leftMargin != Integer.MIN_VALUE ? leftMargin : 0, |
| rightMargin != Integer.MIN_VALUE ? rightMargin : 0, |
| topMargin != Integer.MIN_VALUE ? topMargin : 0, |
| bottomMargin != Integer.MIN_VALUE ? bottomMargin : 0 |
| ); |
| } |
| |
| return null; |
| } |
| |
| // ---- Implementation of IPropertySource |
| // TODO: Get rid of this once the old propertysheet implementation is fully gone |
| |
| @Override |
| public Object getEditableValue() { |
| UiViewElementNode uiView = getUiViewNode(); |
| if (uiView != null) { |
| return ((IPropertySource) uiView).getEditableValue(); |
| } |
| return null; |
| } |
| |
| @Override |
| public IPropertyDescriptor[] getPropertyDescriptors() { |
| UiViewElementNode uiView = getUiViewNode(); |
| if (uiView != null) { |
| return ((IPropertySource) uiView).getPropertyDescriptors(); |
| } |
| return null; |
| } |
| |
| @Override |
| public Object getPropertyValue(Object id) { |
| UiViewElementNode uiView = getUiViewNode(); |
| if (uiView != null) { |
| return ((IPropertySource) uiView).getPropertyValue(id); |
| } |
| return null; |
| } |
| |
| @Override |
| public boolean isPropertySet(Object id) { |
| UiViewElementNode uiView = getUiViewNode(); |
| if (uiView != null) { |
| return ((IPropertySource) uiView).isPropertySet(id); |
| } |
| return false; |
| } |
| |
| @Override |
| public void resetPropertyValue(Object id) { |
| UiViewElementNode uiView = getUiViewNode(); |
| if (uiView != null) { |
| ((IPropertySource) uiView).resetPropertyValue(id); |
| } |
| } |
| |
| @Override |
| public void setPropertyValue(Object id, Object value) { |
| UiViewElementNode uiView = getUiViewNode(); |
| if (uiView != null) { |
| ((IPropertySource) uiView).setPropertyValue(id, value); |
| } |
| } |
| |
| /** |
| * Returns the XML node corresponding to this info, or null if there is no |
| * such XML node. |
| * |
| * @return The XML node corresponding to this info object, or null |
| */ |
| @Nullable |
| public Node getXmlNode() { |
| UiViewElementNode uiView = getUiViewNode(); |
| if (uiView != null) { |
| return uiView.getXmlNode(); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Returns true iff this view info corresponds to a root element. |
| * |
| * @return True iff this is a root view info. |
| */ |
| public boolean isRoot() { |
| // Select the visual element -- unless it's the root. |
| // The root element is the one whose GRAND parent |
| // is null (because the parent will be a -document- |
| // node). |
| |
| // Special case: a gesture overlay is sometimes added as the root, but for all intents |
| // and purposes it is its layout child that is the real root so treat that one as the |
| // root as well (such that the whole layout canvas does not highlight as part of hovers |
| // etc) |
| if (mParent != null |
| && mParent.mName.endsWith(GESTURE_OVERLAY_VIEW) |
| && mParent.isRoot() |
| && mParent.mChildren.size() == 1) { |
| return true; |
| } |
| |
| return mUiViewNode == null || mUiViewNode.getUiParent() == null || |
| mUiViewNode.getUiParent().getUiParent() == null; |
| } |
| |
| /** |
| * Returns true if this {@link CanvasViewInfo} represents an invisible widget that |
| * should be highlighted when selected. This is the case for any layout that is less than the minimum |
| * threshold ({@link #SELECTION_MIN_SIZE}), or any other view that has -0- bounds. |
| * |
| * @return True if this is a tiny layout or invisible view |
| */ |
| public boolean isInvisible() { |
| if (isHidden()) { |
| // Don't expand and highlight hidden widgets |
| return false; |
| } |
| |
| if (mAbsRect.width < SELECTION_MIN_SIZE || mAbsRect.height < SELECTION_MIN_SIZE) { |
| return mUiViewNode != null && (mUiViewNode.getDescriptor().hasChildren() || |
| mAbsRect.width <= 0 || mAbsRect.height <= 0); |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Returns true if this {@link CanvasViewInfo} represents a widget that should be |
| * hidden, such as a {@code <Space>} which are typically not manipulated by the user |
| * through dragging etc. |
| * |
| * @return true if this is a hidden view |
| */ |
| public boolean isHidden() { |
| if (GridLayoutRule.sDebugGridLayout) { |
| return false; |
| } |
| |
| return FQCN_SPACE.equals(mName) || FQCN_SPACE_V7.equals(mName); |
| } |
| |
| /** |
| * Is this {@link CanvasViewInfo} a view that has had its padding inflated in order to |
| * make it visible during selection or dragging? Note that this is NOT considered to |
| * be the case in the explode-all-views mode where all nodes have their padding |
| * increased; it's only used for views that individually exploded because they were |
| * requested visible and they returned true for {@link #isInvisible()}. |
| * |
| * @return True if this is an exploded node. |
| */ |
| public boolean isExploded() { |
| return mExploded; |
| } |
| |
| /** |
| * Mark this {@link CanvasViewInfo} as having been exploded or not. See the |
| * {@link #isExploded()} method for details on what this property means. |
| * |
| * @param exploded New value of the exploded property to mark this info with. |
| */ |
| void setExploded(boolean exploded) { |
| mExploded = exploded; |
| } |
| |
| /** |
| * Returns the info represented as a {@link SimpleElement}. |
| * |
| * @return A {@link SimpleElement} wrapping this info. |
| */ |
| @NonNull |
| SimpleElement toSimpleElement() { |
| |
| UiViewElementNode uiNode = getUiViewNode(); |
| |
| String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor()); |
| String parentFqcn = null; |
| Rect bounds = SwtUtils.toRect(getAbsRect()); |
| Rect parentBounds = null; |
| |
| UiElementNode uiParent = uiNode.getUiParent(); |
| if (uiParent != null) { |
| parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor()); |
| } |
| if (getParent() != null) { |
| parentBounds = SwtUtils.toRect(getParent().getAbsRect()); |
| } |
| |
| SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds); |
| |
| for (UiAttributeNode attr : uiNode.getAllUiAttributes()) { |
| String value = attr.getCurrentValue(); |
| if (value != null && value.length() > 0) { |
| AttributeDescriptor attrDesc = attr.getDescriptor(); |
| SimpleAttribute a = new SimpleAttribute( |
| attrDesc.getNamespaceUri(), |
| attrDesc.getXmlLocalName(), |
| value); |
| e.addAttribute(a); |
| } |
| } |
| |
| for (CanvasViewInfo childVi : getChildren()) { |
| SimpleElement e2 = childVi.toSimpleElement(); |
| if (e2 != null) { |
| e.addInnerElement(e2); |
| } |
| } |
| |
| return e; |
| } |
| |
| /** |
| * Returns the layout url attribute value for the closest surrounding include or |
| * fragment element parent, or null if this {@link CanvasViewInfo} is not rendered as |
| * part of an include or fragment tag. |
| * |
| * @return the layout url attribute value for the surrounding include tag, or null if |
| * not applicable |
| */ |
| @Nullable |
| public String getIncludeUrl() { |
| CanvasViewInfo curr = this; |
| while (curr != null) { |
| if (curr.mUiViewNode != null) { |
| Node node = curr.mUiViewNode.getXmlNode(); |
| if (node != null && node.getNodeType() == Node.ELEMENT_NODE) { |
| String nodeName = node.getNodeName(); |
| if (node.getNamespaceURI() == null |
| && SdkConstants.VIEW_INCLUDE.equals(nodeName)) { |
| // Note: the layout attribute is NOT in the Android namespace |
| Element element = (Element) node; |
| String url = element.getAttribute(SdkConstants.ATTR_LAYOUT); |
| if (url.length() > 0) { |
| return url; |
| } |
| } else if (SdkConstants.VIEW_FRAGMENT.equals(nodeName)) { |
| String url = FragmentMenu.getFragmentLayout(node); |
| if (url != null) { |
| return url; |
| } |
| } |
| } |
| } |
| curr = curr.mParent; |
| } |
| |
| return null; |
| } |
| |
| /** Adds the given {@link CanvasViewInfo} as a new last child of this view */ |
| private void addChild(@NonNull CanvasViewInfo child) { |
| mChildren.add(child); |
| } |
| |
| /** Adds the given {@link CanvasViewInfo} as a child at the given index */ |
| private void addChildAt(int index, @NonNull CanvasViewInfo child) { |
| mChildren.add(index, child); |
| } |
| |
| /** |
| * Removes the given {@link CanvasViewInfo} from the child list of this view, and |
| * returns true if it was successfully removed |
| * |
| * @param child the child to be removed |
| * @return true if it was a child and was removed |
| */ |
| public boolean removeChild(@NonNull CanvasViewInfo child) { |
| return mChildren.remove(child); |
| } |
| |
| @Override |
| public String toString() { |
| return "CanvasViewInfo [name=" + mName + ", node=" + mUiViewNode + "]"; |
| } |
| |
| // ---- Factory functionality ---- |
| |
| /** |
| * Creates a new {@link CanvasViewInfo} hierarchy based on the given {@link ViewInfo} |
| * hierarchy. Note that this will not necessarily create one {@link CanvasViewInfo} |
| * for each {@link ViewInfo}. It will generally only create {@link CanvasViewInfo} |
| * objects for {@link ViewInfo} objects that contain a reference to an |
| * {@link UiViewElementNode}, meaning that it corresponds to an element in the XML |
| * file for this layout file. This is not always the case, such as in the following |
| * scenarios: |
| * <ul> |
| * <li>we link to other layouts with {@code <include>} |
| * <li>the current view is rendered within another view ("Show Included In") such that |
| * the outer file does not correspond to elements in the current included XML layout |
| * <li>on older platforms that don't support {@link Capability#EMBEDDED_LAYOUT} there |
| * is no reference to the {@code <include>} tag |
| * <li>with the {@code <merge>} tag we don't get a reference to the corresponding |
| * element |
| * <ul> |
| * <p> |
| * This method will build up a set of {@link CanvasViewInfo} that corresponds to the |
| * actual <b>selectable</b> views (which are also shown in the Outline). |
| * |
| * @param layoutlib5 if true, the {@link ViewInfo} hierarchy was created by layoutlib |
| * version 5 or higher, which means this algorithm can make certain assumptions |
| * (for example that {@code <merge>} siblings will provide {@link MergeCookie} |
| * references, so we don't have to search for them.) |
| * @param root the root {@link ViewInfo} to build from |
| * @return a {@link CanvasViewInfo} hierarchy |
| */ |
| @NonNull |
| public static Pair<CanvasViewInfo,List<Rectangle>> create(ViewInfo root, boolean layoutlib5) { |
| return new Builder(layoutlib5).create(root); |
| } |
| |
| /** Builder object which walks over a tree of {@link ViewInfo} objects and builds |
| * up a corresponding {@link CanvasViewInfo} hierarchy. */ |
| private static class Builder { |
| public Builder(boolean layoutlib5) { |
| mLayoutLib5 = layoutlib5; |
| } |
| |
| /** |
| * The mapping from nodes that have a {@code <merge>} as a parent in the node |
| * model to their corresponding views |
| */ |
| private Map<UiViewElementNode, List<CanvasViewInfo>> mMergeNodeMap; |
| |
| /** |
| * Whether the ViewInfos are provided by a layout library that is version 5 or |
| * later, since that will allow us to take several shortcuts |
| */ |
| private boolean mLayoutLib5; |
| |
| /** |
| * Creates a hierarchy of {@link CanvasViewInfo} objects and merge bounding |
| * rectangles from the given {@link ViewInfo} hierarchy |
| */ |
| private Pair<CanvasViewInfo,List<Rectangle>> create(ViewInfo root) { |
| Object cookie = root.getCookie(); |
| if (cookie == null) { |
| // Special case: If the root-most view does not have a view cookie, |
| // then we are rendering some outer layout surrounding this layout, and in |
| // that case we must search down the hierarchy for the (possibly multiple) |
| // sub-roots that correspond to elements in this layout, and place them inside |
| // an outer view that has no node. In the outline this item will be used to |
| // show the inclusion-context. |
| CanvasViewInfo rootView = createView(null, root, 0, 0); |
| addKeyedSubtrees(rootView, root, 0, 0); |
| |
| List<Rectangle> includedBounds = new ArrayList<Rectangle>(); |
| for (CanvasViewInfo vi : rootView.getChildren()) { |
| if (vi.getNodeSiblings() == null || vi.isPrimaryNodeSibling()) { |
| includedBounds.add(vi.getAbsRect()); |
| } |
| } |
| |
| // There are <merge> nodes here; see if we can insert it into the hierarchy |
| if (mMergeNodeMap != null) { |
| // Locate all the nodes that have a <merge> as a parent in the node model, |
| // and where the view sits at the top level inside the include-context node. |
| UiViewElementNode merge = null; |
| List<CanvasViewInfo> merged = new ArrayList<CanvasViewInfo>(); |
| for (Map.Entry<UiViewElementNode, List<CanvasViewInfo>> entry : mMergeNodeMap |
| .entrySet()) { |
| UiViewElementNode node = entry.getKey(); |
| if (!hasMergeParent(node)) { |
| continue; |
| } |
| List<CanvasViewInfo> views = entry.getValue(); |
| assert views.size() > 0; |
| CanvasViewInfo view = views.get(0); // primary |
| if (view.getParent() != rootView) { |
| continue; |
| } |
| UiElementNode parent = node.getUiParent(); |
| if (merge != null && parent != merge) { |
| continue; |
| } |
| merge = (UiViewElementNode) parent; |
| merged.add(view); |
| } |
| if (merged.size() > 0) { |
| // Compute a bounding box for the merged views |
| Rectangle absRect = null; |
| for (CanvasViewInfo child : merged) { |
| Rectangle rect = child.getAbsRect(); |
| if (absRect == null) { |
| absRect = rect; |
| } else { |
| absRect = absRect.union(rect); |
| } |
| } |
| |
| CanvasViewInfo mergeView = new CanvasViewInfo(rootView, VIEW_MERGE, null, |
| merge, absRect, absRect, null /* viewInfo */); |
| for (CanvasViewInfo view : merged) { |
| if (rootView.removeChild(view)) { |
| mergeView.addChild(view); |
| } |
| } |
| rootView.addChild(mergeView); |
| } |
| } |
| |
| return Pair.of(rootView, includedBounds); |
| } else { |
| // We have a view key at the top, so just go and create {@link CanvasViewInfo} |
| // objects for each {@link ViewInfo} until we run into a null key. |
| CanvasViewInfo rootView = addKeyedSubtrees(null, root, 0, 0); |
| |
| // Special case: look to see if the root element is really a <merge>, and if so, |
| // manufacture a view for it such that we can target this root element |
| // in drag & drop operations, such that we can show it in the outline, etc |
| if (rootView != null && hasMergeParent(rootView.getUiViewNode())) { |
| CanvasViewInfo merge = new CanvasViewInfo(null, VIEW_MERGE, null, |
| (UiViewElementNode) rootView.getUiViewNode().getUiParent(), |
| rootView.getAbsRect(), rootView.getSelectionRect(), |
| null /* viewInfo */); |
| // Insert the <merge> as the new real root |
| rootView.mParent = merge; |
| merge.addChild(rootView); |
| rootView = merge; |
| } |
| |
| return Pair.of(rootView, null); |
| } |
| } |
| |
| private boolean hasMergeParent(UiViewElementNode rootNode) { |
| UiElementNode rootParent = rootNode.getUiParent(); |
| return (rootParent instanceof UiViewElementNode |
| && VIEW_MERGE.equals(rootParent.getDescriptor().getXmlName())); |
| } |
| |
| /** Creates a {@link CanvasViewInfo} for a given {@link ViewInfo} but does not recurse */ |
| private CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX, |
| int parentY) { |
| Object cookie = root.getCookie(); |
| UiViewElementNode node = null; |
| if (cookie instanceof UiViewElementNode) { |
| node = (UiViewElementNode) cookie; |
| } else if (cookie instanceof MergeCookie) { |
| cookie = ((MergeCookie) cookie).getCookie(); |
| if (cookie instanceof UiViewElementNode) { |
| node = (UiViewElementNode) cookie; |
| CanvasViewInfo view = createView(parent, root, parentX, parentY, node); |
| if (root.getCookie() instanceof MergeCookie && view.mNodeSiblings == null) { |
| List<CanvasViewInfo> v = mMergeNodeMap == null ? |
| null : mMergeNodeMap.get(node); |
| if (v != null) { |
| v.add(view); |
| } else { |
| v = new ArrayList<CanvasViewInfo>(); |
| v.add(view); |
| if (mMergeNodeMap == null) { |
| mMergeNodeMap = |
| new HashMap<UiViewElementNode, List<CanvasViewInfo>>(); |
| } |
| mMergeNodeMap.put(node, v); |
| } |
| view.mNodeSiblings = v; |
| } |
| |
| return view; |
| } |
| } |
| |
| return createView(parent, root, parentX, parentY, node); |
| } |
| |
| /** |
| * Creates a {@link CanvasViewInfo} for a given {@link ViewInfo} but does not recurse. |
| * This method specifies an explicit {@link UiViewElementNode} to use rather than |
| * relying on the view cookie in the info object. |
| */ |
| private CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX, |
| int parentY, UiViewElementNode node) { |
| |
| int x = root.getLeft(); |
| int y = root.getTop(); |
| int w = root.getRight() - x; |
| int h = root.getBottom() - y; |
| |
| x += parentX; |
| y += parentY; |
| |
| Rectangle absRect = new Rectangle(x, y, w - 1, h - 1); |
| |
| if (w < SELECTION_MIN_SIZE) { |
| int d = (SELECTION_MIN_SIZE - w) / 2; |
| x -= d; |
| w += SELECTION_MIN_SIZE - w; |
| } |
| |
| if (h < SELECTION_MIN_SIZE) { |
| int d = (SELECTION_MIN_SIZE - h) / 2; |
| y -= d; |
| h += SELECTION_MIN_SIZE - h; |
| } |
| |
| Rectangle selectionRect = new Rectangle(x, y, w - 1, h - 1); |
| |
| return new CanvasViewInfo(parent, root.getClassName(), root.getViewObject(), node, |
| absRect, selectionRect, root); |
| } |
| |
| /** Create a subtree recursively until you run out of keys */ |
| private CanvasViewInfo createSubtree(CanvasViewInfo parent, ViewInfo viewInfo, |
| int parentX, int parentY) { |
| assert viewInfo.getCookie() != null; |
| |
| CanvasViewInfo view = createView(parent, viewInfo, parentX, parentY); |
| // Bug workaround: Ensure that we never have a child node identical |
| // to its parent node: this can happen for example when rendering a |
| // ZoomControls view where the merge cookies point to the parent. |
| if (parent != null && view.mUiViewNode == parent.mUiViewNode) { |
| return null; |
| } |
| |
| // Process children: |
| parentX += viewInfo.getLeft(); |
| parentY += viewInfo.getTop(); |
| |
| List<ViewInfo> children = viewInfo.getChildren(); |
| |
| if (mLayoutLib5) { |
| for (ViewInfo child : children) { |
| Object cookie = child.getCookie(); |
| if (cookie instanceof UiViewElementNode || cookie instanceof MergeCookie) { |
| CanvasViewInfo childView = createSubtree(view, child, |
| parentX, parentY); |
| if (childView != null) { |
| view.addChild(childView); |
| } |
| } // else: null cookies, adapter item references, etc: No child views. |
| } |
| |
| return view; |
| } |
| |
| // See if we have any missing keys at this level |
| int missingNodes = 0; |
| int mergeNodes = 0; |
| for (ViewInfo child : children) { |
| // Only use children which have a ViewKey of the correct type. |
| // We can't interact with those when they have a null key or |
| // an incompatible type. |
| Object cookie = child.getCookie(); |
| if (!(cookie instanceof UiViewElementNode)) { |
| if (cookie instanceof MergeCookie) { |
| mergeNodes++; |
| } else { |
| missingNodes++; |
| } |
| } |
| } |
| |
| if (missingNodes == 0 && mergeNodes == 0) { |
| // No missing nodes; this is the normal case, and we can just continue to |
| // recursively add our children |
| for (ViewInfo child : children) { |
| CanvasViewInfo childView = createSubtree(view, child, |
| parentX, parentY); |
| view.addChild(childView); |
| } |
| |
| // TBD: Emit placeholder views for keys that have no views? |
| } else { |
| // We don't have keys for one or more of the ViewInfos. There are many |
| // possible causes: we are on an SDK platform that does not support |
| // embedded_layout rendering, or we are including a view with a <merge> |
| // as the root element. |
| |
| UiViewElementNode uiViewNode = view.getUiViewNode(); |
| String containerName = uiViewNode != null |
| ? uiViewNode.getDescriptor().getXmlLocalName() : ""; //$NON-NLS-1$ |
| if (containerName.equals(SdkConstants.VIEW_INCLUDE)) { |
| // This is expected -- we don't WANT to get node keys for the content |
| // of an include since it's in a different file and should be treated |
| // as a single unit that cannot be edited (hence, no CanvasViewInfo |
| // children) |
| } else { |
| // We are getting children with null keys where we don't expect it; |
| // this usually means that we are dealing with an Android platform |
| // that does not support {@link Capability#EMBEDDED_LAYOUT}, or |
| // that there are <merge> tags which are doing surprising things |
| // to the view hierarchy |
| LinkedList<UiViewElementNode> unused = new LinkedList<UiViewElementNode>(); |
| if (uiViewNode != null) { |
| for (UiElementNode child : uiViewNode.getUiChildren()) { |
| if (child instanceof UiViewElementNode) { |
| unused.addLast((UiViewElementNode) child); |
| } |
| } |
| } |
| for (ViewInfo child : children) { |
| Object cookie = child.getCookie(); |
| if (mergeNodes > 0 && cookie instanceof MergeCookie) { |
| cookie = ((MergeCookie) cookie).getCookie(); |
| } |
| if (cookie != null) { |
| unused.remove(cookie); |
| } |
| } |
| |
| if (unused.size() > 0 || mergeNodes > 0) { |
| if (unused.size() == missingNodes) { |
| // The number of unmatched elements and ViewInfos are identical; |
| // it's very likely that they match one to one, so just use these |
| for (ViewInfo child : children) { |
| if (child.getCookie() == null) { |
| // Only create a flat (non-recursive) view |
| CanvasViewInfo childView = createView(view, child, parentX, |
| parentY, unused.removeFirst()); |
| view.addChild(childView); |
| } else { |
| CanvasViewInfo childView = createSubtree(view, child, parentX, |
| parentY); |
| view.addChild(childView); |
| } |
| } |
| } else { |
| // We have an uneven match. In this case we might be dealing |
| // with <merge> etc. |
| // We have no way to associate elements back with the |
| // corresponding <include> tags if there are more than one of |
| // them. That's not a huge tragedy since visually you are not |
| // allowed to edit these anyway; we just need to make a visual |
| // block for these for selection and outline purposes. |
| addMismatched(view, parentX, parentY, children, unused); |
| } |
| } else { |
| // No unused keys, but there are views without keys. |
| // We can't represent these since all views must have node keys |
| // such that you can operate on them. Just ignore these. |
| for (ViewInfo child : children) { |
| if (child.getCookie() != null) { |
| CanvasViewInfo childView = createSubtree(view, child, |
| parentX, parentY); |
| view.addChild(childView); |
| } |
| } |
| } |
| } |
| } |
| |
| return view; |
| } |
| |
| /** |
| * We have various {@link ViewInfo} children with null keys, and/or nodes in |
| * the corresponding UI model that are not referenced by any of the {@link ViewInfo} |
| * objects. This method attempts to account for this, by matching the views in |
| * the right order. |
| */ |
| private void addMismatched(CanvasViewInfo parentView, int parentX, int parentY, |
| List<ViewInfo> children, LinkedList<UiViewElementNode> unused) { |
| UiViewElementNode afterNode = null; |
| UiViewElementNode beforeNode = null; |
| // We have one important clue we can use when matching unused nodes |
| // with views: if we have a view V1 with node N1, and a view V2 with node N2, |
| // then we can only match unknown node UN with unknown node UV if |
| // V1 < UV < V2 and N1 < UN < N2. |
| // We can use these constraints to do the matching, for example by |
| // a simple DAG traversal. However, since the number of unmatched nodes |
| // will typically be very small, we'll just do a simple algorithm here |
| // which checks forwards/backwards whether a match is valid. |
| for (int index = 0, size = children.size(); index < size; index++) { |
| ViewInfo child = children.get(index); |
| if (child.getCookie() != null) { |
| CanvasViewInfo childView = createSubtree(parentView, child, parentX, parentY); |
| if (childView != null) { |
| parentView.addChild(childView); |
| } |
| if (child.getCookie() instanceof UiViewElementNode) { |
| afterNode = (UiViewElementNode) child.getCookie(); |
| } |
| } else { |
| beforeNode = nextViewNode(children, index); |
| |
| // Find first eligible node from unused |
| // TOD: What if there are more eligible? We need to process ALL views |
| // and all nodes in one go here |
| |
| UiViewElementNode matching = null; |
| for (UiViewElementNode candidate : unused) { |
| if (afterNode == null || isAfter(afterNode, candidate)) { |
| if (beforeNode == null || isBefore(beforeNode, candidate)) { |
| matching = candidate; |
| break; |
| } |
| } |
| } |
| |
| if (matching != null) { |
| unused.remove(matching); |
| CanvasViewInfo childView = createView(parentView, child, parentX, parentY, |
| matching); |
| parentView.addChild(childView); |
| afterNode = matching; |
| } else { |
| // We have no node for the view -- what do we do?? |
| // Nothing - we only represent stuff in the outline that is in the |
| // source model, not in the render |
| } |
| } |
| } |
| |
| // Add zero-bounded boxes for all remaining nodes since they need to show |
| // up in the outline, need to be selectable so you can press Delete, etc. |
| if (unused.size() > 0) { |
| Map<UiViewElementNode, Integer> rankMap = |
| new HashMap<UiViewElementNode, Integer>(); |
| Map<UiViewElementNode, CanvasViewInfo> infoMap = |
| new HashMap<UiViewElementNode, CanvasViewInfo>(); |
| UiElementNode parent = unused.get(0).getUiParent(); |
| if (parent != null) { |
| int index = 0; |
| for (UiElementNode child : parent.getUiChildren()) { |
| UiViewElementNode node = (UiViewElementNode) child; |
| rankMap.put(node, index++); |
| } |
| for (CanvasViewInfo child : parentView.getChildren()) { |
| infoMap.put(child.getUiViewNode(), child); |
| } |
| List<Integer> usedIndexes = new ArrayList<Integer>(); |
| for (UiViewElementNode node : unused) { |
| Integer rank = rankMap.get(node); |
| if (rank != null) { |
| usedIndexes.add(rank); |
| } |
| } |
| Collections.sort(usedIndexes); |
| for (int i = usedIndexes.size() - 1; i >= 0; i--) { |
| Integer rank = usedIndexes.get(i); |
| UiViewElementNode found = null; |
| for (UiViewElementNode node : unused) { |
| if (rankMap.get(node) == rank) { |
| found = node; |
| break; |
| } |
| } |
| if (found != null) { |
| Rectangle absRect = new Rectangle(parentX, parentY, 0, 0); |
| String name = found.getDescriptor().getXmlLocalName(); |
| CanvasViewInfo v = new CanvasViewInfo(parentView, name, null, found, |
| absRect, absRect, null /* viewInfo */); |
| // Find corresponding index in the parent view |
| List<CanvasViewInfo> siblings = parentView.getChildren(); |
| int insertPosition = siblings.size(); |
| for (int j = siblings.size() - 1; j >= 0; j--) { |
| CanvasViewInfo sibling = siblings.get(j); |
| UiViewElementNode siblingNode = sibling.getUiViewNode(); |
| if (siblingNode != null) { |
| Integer siblingRank = rankMap.get(siblingNode); |
| if (siblingRank != null && siblingRank < rank) { |
| insertPosition = j + 1; |
| break; |
| } |
| } |
| } |
| parentView.addChildAt(insertPosition, v); |
| unused.remove(found); |
| } |
| } |
| } |
| // Add in any remaining |
| for (UiViewElementNode node : unused) { |
| Rectangle absRect = new Rectangle(parentX, parentY, 0, 0); |
| String name = node.getDescriptor().getXmlLocalName(); |
| CanvasViewInfo v = new CanvasViewInfo(parentView, name, null, node, absRect, |
| absRect, null /* viewInfo */); |
| parentView.addChild(v); |
| } |
| } |
| } |
| |
| private boolean isBefore(UiViewElementNode beforeNode, UiViewElementNode candidate) { |
| UiElementNode parent = candidate.getUiParent(); |
| if (parent != null) { |
| for (UiElementNode sibling : parent.getUiChildren()) { |
| if (sibling == beforeNode) { |
| return false; |
| } else if (sibling == candidate) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| private boolean isAfter(UiViewElementNode afterNode, UiViewElementNode candidate) { |
| UiElementNode parent = candidate.getUiParent(); |
| if (parent != null) { |
| for (UiElementNode sibling : parent.getUiChildren()) { |
| if (sibling == afterNode) { |
| return true; |
| } else if (sibling == candidate) { |
| return false; |
| } |
| } |
| } |
| return false; |
| } |
| |
| private UiViewElementNode nextViewNode(List<ViewInfo> children, int index) { |
| int size = children.size(); |
| for (; index < size; index++) { |
| ViewInfo child = children.get(index); |
| if (child.getCookie() instanceof UiViewElementNode) { |
| return (UiViewElementNode) child.getCookie(); |
| } |
| } |
| |
| return null; |
| } |
| |
| /** Search for a subtree with valid keys and add those subtrees */ |
| private CanvasViewInfo addKeyedSubtrees(CanvasViewInfo parent, ViewInfo viewInfo, |
| int parentX, int parentY) { |
| // We don't include MergeCookies when searching down for the first non-null key, |
| // since this means we are in a "Show Included In" context, and the include tag itself |
| // (which the merge cookie is pointing to) is still in the including-document rather |
| // than the included document. Therefore, we only accept real UiViewElementNodes here, |
| // not MergeCookies. |
| if (viewInfo.getCookie() != null) { |
| CanvasViewInfo subtree = createSubtree(parent, viewInfo, parentX, parentY); |
| if (parent != null && subtree != null) { |
| parent.mChildren.add(subtree); |
| } |
| return subtree; |
| } else { |
| for (ViewInfo child : viewInfo.getChildren()) { |
| addKeyedSubtrees(parent, child, parentX + viewInfo.getLeft(), parentY |
| + viewInfo.getTop()); |
| } |
| |
| return null; |
| } |
| } |
| } |
| } |