aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java326
1 files changed, 326 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java
new file mode 100644
index 000000000..43d52d137
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2011 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.common.layout.relative;
+
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.VALUE_TRUE;
+
+
+import com.android.SdkConstants;
+import static com.android.SdkConstants.ANDROID_URI;
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.IDragElement.IDragAttribute;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.INode.IAttribute;
+import com.android.ide.common.layout.BaseLayoutRule;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Data structure about relative layout relationships which makes it possible to:
+ * <ul>
+ * <li> Quickly determine not just the dependencies on other nodes, but which nodes
+ * depend on this node such that they can be visualized for the selection
+ * <li> Determine if there are cyclic dependencies, and whether a potential move
+ * would result in a cycle
+ * <li> Determine the "depth" of a given node (in terms of how many connections it
+ * is away from a parent edge) such that we can prioritize connections which
+ * minimizes the depth
+ * </ul>
+ */
+class DependencyGraph {
+ /** Format to chain include cycles in: a=>b=>c=>d etc */
+ static final String CHAIN_FORMAT = "%1$s=>%2$s"; //$NON-NLS-1$
+
+ /** Format to chain constraint dependencies: button 1 above button2 etc */
+ private static final String DEPENDENCY_FORMAT = "%1$s %2$s %3$s"; //$NON-NLS-1$
+
+ private final Map<String, ViewData> mIdToView = new HashMap<String, ViewData>();
+ private final Map<INode, ViewData> mNodeToView = new HashMap<INode, ViewData>();
+
+ /** Constructs a new {@link DependencyGraph} for the given relative layout */
+ DependencyGraph(INode layout) {
+ INode[] nodes = layout.getChildren();
+
+ // Parent view:
+ String parentId = layout.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (parentId != null) {
+ parentId = BaseLayoutRule.stripIdPrefix(parentId);
+ } else {
+ parentId = "RelativeLayout"; // For display purposes; we never reference
+ // the parent id from a constraint, only via parent-relative params
+ // like centerInParent
+ }
+ ViewData parentView = new ViewData(layout, parentId);
+ mNodeToView.put(layout, parentView);
+ if (parentId != null) {
+ mIdToView.put(parentId, parentView);
+ }
+
+ for (INode child : nodes) {
+ String id = child.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id != null) {
+ id = BaseLayoutRule.stripIdPrefix(id);
+ }
+ ViewData view = new ViewData(child, id);
+ mNodeToView.put(child, view);
+ if (id != null) {
+ mIdToView.put(id, view);
+ }
+ }
+
+ for (ViewData view : mNodeToView.values()) {
+ for (IAttribute attribute : view.node.getLiveAttributes()) {
+ String name = attribute.getName();
+ ConstraintType type = ConstraintType.fromAttribute(name);
+ if (type != null) {
+ String value = attribute.getValue();
+
+ if (type.targetParent) {
+ if (value.equals(VALUE_TRUE)) {
+ Constraint constraint = new Constraint(type, view, parentView);
+ view.dependsOn.add(constraint);
+ parentView.dependedOnBy.add(constraint);
+ }
+ } else {
+ // id-based constraint.
+ // NOTE: The id could refer to some widget that is NOT a sibling!
+ String targetId = BaseLayoutRule.stripIdPrefix(value);
+ ViewData target = mIdToView.get(targetId);
+ if (target == view) {
+ // Self-reference. RelativeLayout ignores these so it's
+ // not an error like a deeper cycle (where RelativeLayout
+ // will throw an exception), but we might as well warn
+ // the user about it.
+ // TODO: Where do we emit this error?
+ } else if (target != null) {
+ Constraint constraint = new Constraint(type, view, target);
+ view.dependsOn.add(constraint);
+ target.dependedOnBy.add(constraint);
+ } else {
+ // This is valid but we might want to warn...
+ //System.out.println("Warning: no view data found for " + targetId);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public ViewData getView(IDragElement element) {
+ IDragAttribute attribute = element.getAttribute(ANDROID_URI, ATTR_ID);
+ if (attribute != null) {
+ String id = attribute.getValue();
+ id = BaseLayoutRule.stripIdPrefix(id);
+ return getView(id);
+ }
+
+ return null;
+ }
+
+ public ViewData getView(String id) {
+ return mIdToView.get(id);
+ }
+
+ public ViewData getView(INode node) {
+ return mNodeToView.get(node);
+ }
+
+ /**
+ * Returns the set of views that depend on the given node in either the horizontal or
+ * vertical direction
+ *
+ * @param nodes the set of nodes that we want to compute the transitive dependencies
+ * for
+ * @param vertical if true, look for vertical edge dependencies, otherwise look for
+ * horizontal edge dependencies
+ * @return the set of nodes that directly or indirectly depend on the given nodes in
+ * the given direction
+ */
+ public Set<INode> dependsOn(Collection<? extends INode> nodes, boolean vertical) {
+ List<ViewData> reachable = new ArrayList<ViewData>();
+
+ // Traverse the graph of constraints and determine all nodes affected by
+ // this node
+ Set<ViewData> visiting = new HashSet<ViewData>();
+ for (INode node : nodes) {
+ ViewData view = mNodeToView.get(node);
+ if (view != null) {
+ findBackwards(view, visiting, reachable, vertical, view);
+ }
+ }
+
+ Set<INode> dependents = new HashSet<INode>(reachable.size());
+
+ for (ViewData v : reachable) {
+ dependents.add(v.node);
+ }
+
+ return dependents;
+ }
+
+ private void findBackwards(ViewData view,
+ Set<ViewData> visiting, List<ViewData> reachable,
+ boolean vertical, ViewData start) {
+ visiting.add(view);
+ reachable.add(view);
+
+ for (Constraint constraint : view.dependedOnBy) {
+ if (vertical && !constraint.type.verticalEdge) {
+ continue;
+ } else if (!vertical && !constraint.type.horizontalEdge) {
+ continue;
+ }
+
+ assert constraint.to == view;
+ ViewData from = constraint.from;
+ if (visiting.contains(from)) {
+ // Cycle - what do we do to highlight this?
+ List<Constraint> path = getPathTo(start.node, view.node, vertical);
+ if (path != null) {
+ // TODO: display to the user somehow. We need log access for the
+ // view rules.
+ System.out.println(Constraint.describePath(path, null, null));
+ }
+ } else {
+ findBackwards(from, visiting, reachable, vertical, start);
+ }
+ }
+
+ visiting.remove(view);
+ }
+
+ public List<Constraint> getPathTo(INode from, INode to, boolean vertical) {
+ // Traverse the graph of constraints and determine all nodes affected by
+ // this node
+ Set<ViewData> visiting = new HashSet<ViewData>();
+ List<Constraint> path = new ArrayList<Constraint>();
+ ViewData view = mNodeToView.get(from);
+ if (view != null) {
+ return findForwards(view, visiting, path, vertical, to);
+ }
+
+ return null;
+ }
+
+ private List<Constraint> findForwards(ViewData view, Set<ViewData> visiting,
+ List<Constraint> path, boolean vertical, INode target) {
+ visiting.add(view);
+
+ for (Constraint constraint : view.dependsOn) {
+ if (vertical && !constraint.type.verticalEdge) {
+ continue;
+ } else if (!vertical && !constraint.type.horizontalEdge) {
+ continue;
+ }
+
+ try {
+ path.add(constraint);
+
+ if (constraint.to.node == target) {
+ return new ArrayList<Constraint>(path);
+ }
+
+ assert constraint.from == view;
+ ViewData to = constraint.to;
+ if (visiting.contains(to)) {
+ // CYCLE!
+ continue;
+ }
+
+ List<Constraint> chain = findForwards(to, visiting, path, vertical, target);
+ if (chain != null) {
+ return chain;
+ }
+ } finally {
+ path.remove(constraint);
+ }
+ }
+
+ visiting.remove(view);
+
+ return null;
+ }
+
+ /**
+ * Info about a specific widget child of a relative layout and its constraints. This
+ * is a node in the dependency graph.
+ */
+ static class ViewData {
+ public final INode node;
+ public final String id;
+ public final List<Constraint> dependsOn = new ArrayList<Constraint>(4);
+ public final List<Constraint> dependedOnBy = new ArrayList<Constraint>(8);
+
+ ViewData(INode node, String id) {
+ this.node = node;
+ this.id = id;
+ }
+ }
+
+ /**
+ * Info about a specific constraint between two widgets in a relative layout. This is
+ * an edge in the dependency graph.
+ */
+ static class Constraint {
+ public final ConstraintType type;
+ public final ViewData from;
+ public final ViewData to;
+
+ // TODO: Initialize depth -- should be computed independently for top, left, etc.
+ // We can use this in GuidelineHandler.MatchComparator to prefer matches that
+ // are closer to a parent edge:
+ //public int depth;
+
+ Constraint(ConstraintType type, ViewData from, ViewData to) {
+ this.type = type;
+ this.from = from;
+ this.to = to;
+ }
+
+ static String describePath(List<Constraint> path, String newName, String newId) {
+ String s = "";
+ for (int i = path.size() - 1; i >= 0; i--) {
+ Constraint constraint = path.get(i);
+ String suffix = (i == path.size() -1) ? constraint.to.id : s;
+ s = String.format(DEPENDENCY_FORMAT, constraint.from.id,
+ stripLayoutAttributePrefix(constraint.type.name), suffix);
+ }
+
+ if (newName != null) {
+ s = String.format(DEPENDENCY_FORMAT, s, stripLayoutAttributePrefix(newName),
+ BaseLayoutRule.stripIdPrefix(newId));
+ }
+
+ return s;
+ }
+
+ private static String stripLayoutAttributePrefix(String name) {
+ if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
+ return name.substring(ATTR_LAYOUT_RESOURCE_PREFIX.length());
+ }
+
+ return name;
+ }
+ }
+}