blob: d247e28d75ae2b6d3067160b83fede1bf5b20c01 [file] [log] [blame]
/*
* Copyright (C) 2010 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.ATTR_ID;
import static com.android.SdkConstants.VIEW_MERGE;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.api.INode;
import com.android.ide.common.rendering.api.RenderSession;
import com.android.ide.common.rendering.api.ViewInfo;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.utils.Pair;
import org.eclipse.swt.graphics.Rectangle;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.RandomAccess;
import java.util.Set;
/**
* The view hierarchy class manages a set of view info objects and performs find
* operations on this set.
*/
public class ViewHierarchy {
private static final boolean DUMP_INFO = false;
private LayoutCanvas mCanvas;
/**
* Constructs a new {@link ViewHierarchy} tied to the given
* {@link LayoutCanvas}.
*
* @param canvas The {@link LayoutCanvas} to create a {@link ViewHierarchy}
* for.
*/
public ViewHierarchy(LayoutCanvas canvas) {
mCanvas = canvas;
}
/**
* The CanvasViewInfo root created by the last call to {@link #setSession}
* with a valid layout.
* <p/>
* This <em>can</em> be null to indicate we're dealing with an empty document with
* no root node. Null here does not mean the result was invalid, merely that the XML
* had no content to display -- we need to treat an empty document as valid so that
* we can drop new items in it.
*/
private CanvasViewInfo mLastValidViewInfoRoot;
/**
* True when the last {@link #setSession} provided a valid {@link LayoutScene}.
* <p/>
* When false this means the canvas is displaying an out-dated result image & bounds and some
* features should be disabled accordingly such a drag'n'drop.
* <p/>
* Note that an empty document (with a null {@link #mLastValidViewInfoRoot}) is considered
* valid since it is an acceptable drop target.
*/
private boolean mIsResultValid;
/**
* A list of invisible parents (see {@link CanvasViewInfo#isInvisible()} for
* details) in the current view hierarchy.
*/
private final List<CanvasViewInfo> mInvisibleParents = new ArrayList<CanvasViewInfo>();
/**
* A read-only view of {@link #mInvisibleParents}; note that this is NOT a copy so it
* reflects updates to the underlying {@link #mInvisibleParents} list.
*/
private final List<CanvasViewInfo> mInvisibleParentsReadOnly =
Collections.unmodifiableList(mInvisibleParents);
/**
* Flag which records whether or not we have any exploded parent nodes in this
* view hierarchy. This is used to track whether or not we need to recompute the
* layout when we exit show-all-invisible-parents mode (see
* {@link LayoutCanvas#showInvisibleViews}).
*/
private boolean mExplodedParents;
/**
* Bounds of included views in the current view hierarchy when rendered in other context
*/
private List<Rectangle> mIncludedBounds;
/** The render session for the current view hierarchy */
private RenderSession mSession;
/** Map from nodes to canvas view infos */
private Map<UiViewElementNode, CanvasViewInfo> mNodeToView = Collections.emptyMap();
/** Map from DOM nodes to canvas view infos */
private Map<Node, CanvasViewInfo> mDomNodeToView = Collections.emptyMap();
/**
* Disposes the view hierarchy content.
*/
public void dispose() {
if (mSession != null) {
mSession.dispose();
mSession = null;
}
}
/**
* Sets the result of the layout rendering. The result object indicates if the layout
* rendering succeeded. If it did, it contains a bitmap and the objects rectangles.
*
* Implementation detail: the bridge's computeLayout() method already returns a newly
* allocated ILayourResult. That means we can keep this result and hold on to it
* when it is valid.
*
* @param session The new session, either valid or not.
* @param explodedNodes The set of individual nodes the layout computer was asked to
* explode. Note that these are independent of the explode-all mode where
* all views are exploded; this is used only for the mode (
* {@link LayoutCanvas#showInvisibleViews}) where individual invisible
* nodes are padded during certain interactions.
*/
/* package */ void setSession(RenderSession session, Set<UiElementNode> explodedNodes,
boolean layoutlib5) {
// replace the previous scene, so the previous scene must be disposed.
if (mSession != null) {
mSession.dispose();
}
mSession = session;
mIsResultValid = (session != null && session.getResult().isSuccess());
mExplodedParents = false;
mNodeToView = new HashMap<UiViewElementNode, CanvasViewInfo>(50);
if (mIsResultValid && session != null) {
List<ViewInfo> rootList = session.getRootViews();
Pair<CanvasViewInfo,List<Rectangle>> infos = null;
if (rootList == null || rootList.size() == 0) {
// Special case: Look to see if this is really an empty <merge> view,
// which shows up without any ViewInfos in the merge. In that case we
// want to manufacture an empty view, such that we can target the view
// via drag & drop, etc.
if (hasMergeRoot()) {
ViewInfo mergeRoot = createMergeInfo(session);
infos = CanvasViewInfo.create(mergeRoot, layoutlib5);
} else {
infos = null;
}
} else {
if (rootList.size() > 1 && hasMergeRoot()) {
ViewInfo mergeRoot = createMergeInfo(session);
mergeRoot.setChildren(rootList);
infos = CanvasViewInfo.create(mergeRoot, layoutlib5);
} else {
ViewInfo root = rootList.get(0);
if (root != null) {
infos = CanvasViewInfo.create(root, layoutlib5);
if (DUMP_INFO) {
dump(session, root, 0);
}
} else {
infos = null;
}
}
}
if (infos != null) {
mLastValidViewInfoRoot = infos.getFirst();
mIncludedBounds = infos.getSecond();
if (mLastValidViewInfoRoot.getUiViewNode() == null &&
mLastValidViewInfoRoot.getChildren().isEmpty()) {
GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor();
if (editor.getIncludedWithin() != null) {
// Somehow, this view was supposed to be rendered within another
// view, yet this view was rendered as part of the other view.
// In that case, abort attempting to show included in; clear the
// include context and trigger a standalone re-render.
editor.showIn(null);
return;
}
}
} else {
mLastValidViewInfoRoot = null;
mIncludedBounds = null;
}
updateNodeProxies(mLastValidViewInfoRoot);
// Update the data structures related to tracking invisible and exploded nodes.
// We need to find the {@link CanvasViewInfo} objects that correspond to
// the passed in {@link UiElementNode} keys that were re-rendered, and mark
// them as exploded and store them in a list for rendering.
mExplodedParents = false;
mInvisibleParents.clear();
addInvisibleParents(mLastValidViewInfoRoot, explodedNodes);
mDomNodeToView = new HashMap<Node, CanvasViewInfo>(mNodeToView.size());
for (Map.Entry<UiViewElementNode, CanvasViewInfo> entry : mNodeToView.entrySet()) {
mDomNodeToView.put(entry.getKey().getXmlNode(), entry.getValue());
}
// Update the selection
mCanvas.getSelectionManager().sync();
} else {
mIncludedBounds = null;
mInvisibleParents.clear();
mDomNodeToView = Collections.emptyMap();
}
}
private ViewInfo createMergeInfo(RenderSession session) {
BufferedImage image = session.getImage();
ControlPoint imageSize = ControlPoint.create(mCanvas,
mCanvas.getHorizontalTransform().getMargin() + image.getWidth(),
mCanvas.getVerticalTransform().getMargin() + image.getHeight());
LayoutPoint layoutSize = imageSize.toLayout();
UiDocumentNode model = mCanvas.getEditorDelegate().getUiRootNode();
List<UiElementNode> children = model.getUiChildren();
return new ViewInfo(VIEW_MERGE, children.get(0), 0, 0, layoutSize.x, layoutSize.y);
}
/**
* Returns true if this view hierarchy corresponds to an editor that has a {@code
* <merge>} tag at the root
*
* @return true if there is a {@code <merge>} at the root of this editor's document
*/
private boolean hasMergeRoot() {
UiDocumentNode model = mCanvas.getEditorDelegate().getUiRootNode();
if (model != null) {
List<UiElementNode> children = model.getUiChildren();
if (children != null && children.size() > 0
&& VIEW_MERGE.equals(children.get(0).getDescriptor().getXmlName())) {
return true;
}
}
return false;
}
/**
* Creates or updates the node proxy for this canvas view info.
* <p/>
* Since proxies are reused, this will update the bounds of an existing proxy when the
* canvas is refreshed and a view changes position or size.
* <p/>
* This is a recursive call that updates the whole hierarchy starting at the given
* view info.
*/
private void updateNodeProxies(CanvasViewInfo vi) {
if (vi == null) {
return;
}
UiViewElementNode key = vi.getUiViewNode();
if (key != null) {
mCanvas.getNodeFactory().create(vi);
mNodeToView.put(key, vi);
}
for (CanvasViewInfo child : vi.getChildren()) {
updateNodeProxies(child);
}
}
/**
* Make a pass over the view hierarchy and look for two things:
* <ol>
* <li>Invisible parents. These are nodes that can hold children and have empty
* bounds. These are then added to the {@link #mInvisibleParents} list.
* <li>Exploded nodes. These are nodes that were previously marked as invisible, and
* subsequently rendered by a recomputed layout. They now no longer have empty bounds,
* but should be specially marked via {@link CanvasViewInfo#setExploded} such that we
* for example in selection operations can determine if we need to recompute the
* layout.
* </ol>
*
* @param vi
* @param invisibleNodes
*/
private void addInvisibleParents(CanvasViewInfo vi, Set<UiElementNode> invisibleNodes) {
if (vi == null) {
return;
}
if (vi.isInvisible()) {
mInvisibleParents.add(vi);
} else if (invisibleNodes != null) {
UiViewElementNode key = vi.getUiViewNode();
if (key != null && invisibleNodes.contains(key)) {
vi.setExploded(true);
mExplodedParents = true;
mInvisibleParents.add(vi);
}
}
for (CanvasViewInfo child : vi.getChildren()) {
addInvisibleParents(child, invisibleNodes);
}
}
/**
* Returns the current {@link RenderSession}.
* @return the session or null if none have been set.
*/
public RenderSession getSession() {
return mSession;
}
/**
* Returns true when the last {@link #setSession} provided a valid
* {@link RenderSession}.
* <p/>
* When false this means the canvas is displaying an out-dated result image & bounds and some
* features should be disabled accordingly such a drag'n'drop.
* <p/>
* Note that an empty document (with a null {@link #getRoot()}) is considered
* valid since it is an acceptable drop target.
* @return True when this {@link ViewHierarchy} contains a valid hierarchy of views.
*/
public boolean isValid() {
return mIsResultValid;
}
/**
* Returns true if the last valid content of the canvas represents an empty document.
* @return True if the last valid content of the canvas represents an empty document.
*/
public boolean isEmpty() {
return mLastValidViewInfoRoot == null;
}
/**
* Returns true if we have parents in this hierarchy that are invisible (e.g. because
* they have no children and zero layout bounds).
*
* @return True if we have invisible parents.
*/
public boolean hasInvisibleParents() {
return mInvisibleParents.size() > 0;
}
/**
* Returns true if we have views that were exploded during rendering
* @return True if we have exploded parents
*/
public boolean hasExplodedParents() {
return mExplodedParents;
}
/** Locates and return any views that overlap the given selection rectangle.
* @param topLeft The top left corner of the selection rectangle.
* @param bottomRight The bottom right corner of the selection rectangle.
* @return A collection of {@link CanvasViewInfo} objects that overlap the
* rectangle.
*/
public Collection<CanvasViewInfo> findWithin(
LayoutPoint topLeft,
LayoutPoint bottomRight) {
Rectangle selectionRectangle = new Rectangle(topLeft.x, topLeft.y, bottomRight.x
- topLeft.x, bottomRight.y - topLeft.y);
List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
addWithin(mLastValidViewInfoRoot, selectionRectangle, infos);
return infos;
}
/**
* Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
* <p/>
* Tries to find the inner most child matching the given x,y coordinates in the view
* info sub-tree. This uses the potentially-expanded selection bounds.
*
* Returns null if not found.
*/
private void addWithin(
CanvasViewInfo canvasViewInfo,
Rectangle canvasRectangle,
List<CanvasViewInfo> infos) {
if (canvasViewInfo == null) {
return;
}
Rectangle r = canvasViewInfo.getSelectionRect();
if (canvasRectangle.intersects(r)) {
// try to find a matching child first
for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
addWithin(child, canvasRectangle, infos);
}
if (canvasViewInfo != mLastValidViewInfoRoot) {
infos.add(canvasViewInfo);
}
}
}
/**
* Locates and returns the {@link CanvasViewInfo} corresponding to the given
* node, or null if it cannot be found.
*
* @param node The node we want to find a corresponding
* {@link CanvasViewInfo} for.
* @return The {@link CanvasViewInfo} corresponding to the given node, or
* null if no match was found.
*/
@Nullable
public CanvasViewInfo findViewInfoFor(@Nullable Node node) {
CanvasViewInfo vi = mDomNodeToView.get(node);
if (vi == null) {
if (node == null) {
return null;
} else if (node.getNodeType() == Node.TEXT_NODE) {
return mDomNodeToView.get(node.getParentNode());
} else if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
return mDomNodeToView.get(((Attr) node).getOwnerElement());
} else if (node.getNodeType() == Node.DOCUMENT_NODE) {
return mDomNodeToView.get(((Document) node).getDocumentElement());
}
}
return vi;
}
/**
* Tries to find the inner most child matching the given x,y coordinates in
* the view info sub-tree, starting at the last know view info root. This
* uses the potentially-expanded selection bounds.
* <p/>
* Returns null if not found or if there's no view info root.
*
* @param p The point at which to look for the deepest match in the view
* hierarchy
* @return A {@link CanvasViewInfo} that intersects the given point, or null
* if nothing was found.
*/
public CanvasViewInfo findViewInfoAt(LayoutPoint p) {
if (mLastValidViewInfoRoot == null) {
return null;
}
return findViewInfoAt_Recursive(p, mLastValidViewInfoRoot);
}
/**
* Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
* <p/>
* Tries to find the inner most child matching the given x,y coordinates in the view
* info sub-tree. This uses the potentially-expanded selection bounds.
*
* Returns null if not found.
*/
private CanvasViewInfo findViewInfoAt_Recursive(LayoutPoint p, CanvasViewInfo canvasViewInfo) {
if (canvasViewInfo == null) {
return null;
}
Rectangle r = canvasViewInfo.getSelectionRect();
if (r.contains(p.x, p.y)) {
// try to find a matching child first
// Iterate in REVERSE z order such that siblings on top
// are checked before earlier siblings (this matters in layouts like
// FrameLayout and in <merge> contexts where the views are sitting on top
// of each other and we want to select the same view as the one drawn
// on top of the others
List<CanvasViewInfo> children = canvasViewInfo.getChildren();
assert children instanceof RandomAccess;
for (int i = children.size() - 1; i >= 0; i--) {
CanvasViewInfo child = children.get(i);
CanvasViewInfo v = findViewInfoAt_Recursive(p, child);
if (v != null) {
return v;
}
}
// if no children matched, this is the view that we're looking for
return canvasViewInfo;
}
return null;
}
/**
* Returns a list of all the possible alternatives for a given view at the given
* position. This is used to build and manage the "alternate" selection that cycles
* around the parents or children of the currently selected element.
*/
/* package */ List<CanvasViewInfo> findAltViewInfoAt(LayoutPoint p) {
if (mLastValidViewInfoRoot != null) {
return findAltViewInfoAt_Recursive(p, mLastValidViewInfoRoot, null);
}
return null;
}
/**
* Internal recursive version of {@link #findAltViewInfoAt(int, int, CanvasViewInfo)}.
* Please don't use directly.
*/
private List<CanvasViewInfo> findAltViewInfoAt_Recursive(
LayoutPoint p, CanvasViewInfo parent, List<CanvasViewInfo> outList) {
Rectangle r;
if (outList == null) {
outList = new ArrayList<CanvasViewInfo>();
if (parent != null) {
// add the parent root only once
r = parent.getSelectionRect();
if (r.contains(p.x, p.y)) {
outList.add(parent);
}
}
}
if (parent != null && !parent.getChildren().isEmpty()) {
// then add all children that match the position
for (CanvasViewInfo child : parent.getChildren()) {
r = child.getSelectionRect();
if (r.contains(p.x, p.y)) {
outList.add(child);
}
}
// finally recurse in the children
for (CanvasViewInfo child : parent.getChildren()) {
r = child.getSelectionRect();
if (r.contains(p.x, p.y)) {
findAltViewInfoAt_Recursive(p, child, outList);
}
}
}
return outList;
}
/**
* Locates and returns the {@link CanvasViewInfo} corresponding to the given
* node, or null if it cannot be found.
*
* @param node The node we want to find a corresponding
* {@link CanvasViewInfo} for.
* @return The {@link CanvasViewInfo} corresponding to the given node, or
* null if no match was found.
*/
public CanvasViewInfo findViewInfoFor(INode node) {
return findViewInfoFor((NodeProxy) node);
}
/**
* Tries to find a child with the same view key in the view info sub-tree.
* Returns null if not found.
*
* @param viewKey The view key that a matching {@link CanvasViewInfo} should
* have as its key.
* @return A {@link CanvasViewInfo} matching the given key, or null if not
* found.
*/
public CanvasViewInfo findViewInfoFor(UiElementNode viewKey) {
return mNodeToView.get(viewKey);
}
/**
* Tries to find a child with the given node proxy as the view key.
* Returns null if not found.
*
* @param proxy The view key that a matching {@link CanvasViewInfo} should
* have as its key.
* @return A {@link CanvasViewInfo} matching the given key, or null if not
* found.
*/
@Nullable
public CanvasViewInfo findViewInfoFor(@Nullable NodeProxy proxy) {
if (proxy == null) {
return null;
}
return mNodeToView.get(proxy.getNode());
}
/**
* Returns a list of ALL ViewInfos (possibly excluding the root, depending
* on the parameter for that).
*
* @param includeRoot If true, include the root in the list, otherwise
* exclude it (but include all its children)
* @return A list of canvas view infos.
*/
public List<CanvasViewInfo> findAllViewInfos(boolean includeRoot) {
List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
if (mIsResultValid && mLastValidViewInfoRoot != null) {
findAllViewInfos(infos, mLastValidViewInfoRoot, includeRoot);
}
return infos;
}
private void findAllViewInfos(List<CanvasViewInfo> result, CanvasViewInfo canvasViewInfo,
boolean includeRoot) {
if (canvasViewInfo != null) {
if (includeRoot || !canvasViewInfo.isRoot()) {
result.add(canvasViewInfo);
}
for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
findAllViewInfos(result, child, true);
}
}
}
/**
* Returns the root of the view hierarchy, if any (could be null, for example
* on rendering failure).
*
* @return The current view hierarchy, or null
*/
public CanvasViewInfo getRoot() {
return mLastValidViewInfoRoot;
}
/**
* Returns a collection of views that have zero bounds and that correspond to empty
* parents. Note that the views may not actually have zero bounds; in particular, if
* they are exploded ({@link CanvasViewInfo#isExploded()}, then they will have the
* bounds of a shown invisible node. Therefore, this method returns the views that
* would be invisible in a real rendering of the scene.
*
* @return A collection of empty parent views.
*/
public List<CanvasViewInfo> getInvisibleViews() {
return mInvisibleParentsReadOnly;
}
/**
* Returns the invisible nodes (the {@link UiElementNode} objects corresponding
* to the {@link CanvasViewInfo} objects returned from {@link #getInvisibleViews()}.
* We are pulling out the nodes since they preserve their identity across layout
* rendering, and in particular we return it as a set such that the layout renderer
* can perform quick identity checks when looking up attribute values during the
* rendering process.
*
* @return A set of the invisible nodes.
*/
public Set<UiElementNode> getInvisibleNodes() {
if (mInvisibleParents.size() == 0) {
return Collections.emptySet();
}
Set<UiElementNode> nodes = new HashSet<UiElementNode>(mInvisibleParents.size());
for (CanvasViewInfo info : mInvisibleParents) {
UiViewElementNode node = info.getUiViewNode();
if (node != null) {
nodes.add(node);
}
}
return nodes;
}
/**
* Returns the list of bounds for included views in the current view hierarchy. Can be null
* when there are no included views.
*
* @return a list of included view bounds, or null
*/
public List<Rectangle> getIncludedBounds() {
return mIncludedBounds;
}
/**
* Returns a map of the default properties for the given view object in this session
*
* @param viewObject the object to look up the properties map for
* @return the map of properties, or null if not found
*/
@Nullable
public Map<String, String> getDefaultProperties(@NonNull Object viewObject) {
if (mSession != null) {
return mSession.getDefaultProperties(viewObject);
}
return null;
}
/**
* Dumps a {@link ViewInfo} hierarchy to stdout
*
* @param session the corresponding session, if any
* @param info the {@link ViewInfo} object to dump
* @param depth the depth to indent it to
*/
public static void dump(RenderSession session, ViewInfo info, int depth) {
if (DUMP_INFO) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < depth; i++) {
sb.append(" "); //$NON-NLS-1$
}
sb.append(info.getClassName());
sb.append(" ["); //$NON-NLS-1$
sb.append(info.getLeft());
sb.append(","); //$NON-NLS-1$
sb.append(info.getTop());
sb.append(","); //$NON-NLS-1$
sb.append(info.getRight());
sb.append(","); //$NON-NLS-1$
sb.append(info.getBottom());
sb.append("]"); //$NON-NLS-1$
Object cookie = info.getCookie();
if (cookie instanceof UiViewElementNode) {
sb.append(" "); //$NON-NLS-1$
UiViewElementNode node = (UiViewElementNode) cookie;
sb.append("<"); //$NON-NLS-1$
sb.append(node.getDescriptor().getXmlName());
sb.append(">"); //$NON-NLS-1$
String id = node.getAttributeValue(ATTR_ID);
if (id != null && !id.isEmpty()) {
sb.append(" ");
sb.append(id);
}
} else if (cookie != null) {
sb.append(" " + cookie); //$NON-NLS-1$
}
/* Display defaults?
if (info.getViewObject() != null) {
Map<String, String> defaults = session.getDefaultProperties(info.getCookie());
sb.append(" - defaults: "); //$NON-NLS-1$
sb.append(defaults);
sb.append('\n');
}
*/
System.out.println(sb.toString());
for (ViewInfo child : info.getChildren()) {
dump(session, child, depth + 1);
}
}
}
}