1 /*
2  * Copyright (C) 2011 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 package com.android.ide.common.layout.relative;
17 
18 import static com.android.SdkConstants.ATTR_ID;
19 import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
20 import static com.android.SdkConstants.VALUE_TRUE;
21 
22 
23 import com.android.SdkConstants;
24 import static com.android.SdkConstants.ANDROID_URI;
25 import com.android.ide.common.api.IDragElement;
26 import com.android.ide.common.api.IDragElement.IDragAttribute;
27 import com.android.ide.common.api.INode;
28 import com.android.ide.common.api.INode.IAttribute;
29 import com.android.ide.common.layout.BaseLayoutRule;
30 
31 import java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.HashMap;
34 import java.util.HashSet;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Set;
38 
39 /**
40  * Data structure about relative layout relationships which makes it possible to:
41  * <ul>
42  * <li> Quickly determine not just the dependencies on other nodes, but which nodes
43  *    depend on this node such that they can be visualized for the selection
44  * <li> Determine if there are cyclic dependencies, and whether a potential move
45  *    would result in a cycle
46  * <li> Determine the "depth" of a given node (in terms of how many connections it
47  *     is away from a parent edge) such that we can prioritize connections which
48  *     minimizes the depth
49  * </ul>
50  */
51 class DependencyGraph {
52     /** Format to chain include cycles in: a=>b=>c=>d etc */
53     static final String CHAIN_FORMAT = "%1$s=>%2$s"; //$NON-NLS-1$
54 
55     /** Format to chain constraint dependencies: button 1 above button2 etc */
56     private static final String DEPENDENCY_FORMAT = "%1$s %2$s %3$s"; //$NON-NLS-1$
57 
58     private final Map<String, ViewData> mIdToView = new HashMap<String, ViewData>();
59     private final Map<INode, ViewData> mNodeToView = new HashMap<INode, ViewData>();
60 
61     /** Constructs a new {@link DependencyGraph} for the given relative layout */
DependencyGraph(INode layout)62     DependencyGraph(INode layout) {
63         INode[] nodes = layout.getChildren();
64 
65         // Parent view:
66         String parentId = layout.getStringAttr(ANDROID_URI, ATTR_ID);
67         if (parentId != null) {
68             parentId = BaseLayoutRule.stripIdPrefix(parentId);
69         } else {
70             parentId = "RelativeLayout"; // For display purposes; we never reference
71             // the parent id from a constraint, only via parent-relative params
72             // like centerInParent
73         }
74         ViewData parentView = new ViewData(layout, parentId);
75         mNodeToView.put(layout, parentView);
76         if (parentId != null) {
77             mIdToView.put(parentId, parentView);
78         }
79 
80         for (INode child : nodes) {
81             String id = child.getStringAttr(ANDROID_URI, ATTR_ID);
82             if (id != null) {
83                 id = BaseLayoutRule.stripIdPrefix(id);
84             }
85             ViewData view = new ViewData(child, id);
86             mNodeToView.put(child, view);
87             if (id != null) {
88                 mIdToView.put(id, view);
89             }
90         }
91 
92         for (ViewData view : mNodeToView.values()) {
93             for (IAttribute attribute : view.node.getLiveAttributes()) {
94                 String name = attribute.getName();
95                 ConstraintType type = ConstraintType.fromAttribute(name);
96                 if (type != null) {
97                     String value = attribute.getValue();
98 
99                     if (type.targetParent) {
100                         if (value.equals(VALUE_TRUE)) {
101                             Constraint constraint = new Constraint(type, view, parentView);
102                             view.dependsOn.add(constraint);
103                             parentView.dependedOnBy.add(constraint);
104                         }
105                     } else {
106                         // id-based constraint.
107                         // NOTE: The id could refer to some widget that is NOT a sibling!
108                         String targetId = BaseLayoutRule.stripIdPrefix(value);
109                         ViewData target = mIdToView.get(targetId);
110                         if (target == view) {
111                             // Self-reference. RelativeLayout ignores these so it's
112                             // not an error like a deeper cycle (where RelativeLayout
113                             // will throw an exception), but we might as well warn
114                             // the user about it.
115                             // TODO: Where do we emit this error?
116                         } else if (target != null) {
117                             Constraint constraint = new Constraint(type, view, target);
118                             view.dependsOn.add(constraint);
119                             target.dependedOnBy.add(constraint);
120                         } else {
121                             // This is valid but we might want to warn...
122                             //System.out.println("Warning: no view data found for " + targetId);
123                         }
124                     }
125                 }
126             }
127         }
128     }
129 
getView(IDragElement element)130     public ViewData getView(IDragElement element) {
131         IDragAttribute attribute = element.getAttribute(ANDROID_URI, ATTR_ID);
132         if (attribute != null) {
133             String id = attribute.getValue();
134             id = BaseLayoutRule.stripIdPrefix(id);
135             return getView(id);
136         }
137 
138         return null;
139     }
140 
getView(String id)141     public ViewData getView(String id) {
142         return mIdToView.get(id);
143     }
144 
getView(INode node)145     public ViewData getView(INode node) {
146         return mNodeToView.get(node);
147     }
148 
149     /**
150      * Returns the set of views that depend on the given node in either the horizontal or
151      * vertical direction
152      *
153      * @param nodes the set of nodes that we want to compute the transitive dependencies
154      *            for
155      * @param vertical if true, look for vertical edge dependencies, otherwise look for
156      *            horizontal edge dependencies
157      * @return the set of nodes that directly or indirectly depend on the given nodes in
158      *         the given direction
159      */
dependsOn(Collection<? extends INode> nodes, boolean vertical)160     public Set<INode> dependsOn(Collection<? extends INode> nodes, boolean vertical) {
161         List<ViewData> reachable = new ArrayList<ViewData>();
162 
163         // Traverse the graph of constraints and determine all nodes affected by
164         // this node
165         Set<ViewData> visiting = new HashSet<ViewData>();
166         for (INode node : nodes) {
167             ViewData view = mNodeToView.get(node);
168             if (view != null) {
169                 findBackwards(view, visiting, reachable, vertical, view);
170             }
171         }
172 
173         Set<INode> dependents = new HashSet<INode>(reachable.size());
174 
175         for (ViewData v : reachable) {
176             dependents.add(v.node);
177         }
178 
179         return dependents;
180     }
181 
findBackwards(ViewData view, Set<ViewData> visiting, List<ViewData> reachable, boolean vertical, ViewData start)182     private void findBackwards(ViewData view,
183             Set<ViewData> visiting, List<ViewData> reachable,
184             boolean vertical, ViewData start) {
185         visiting.add(view);
186         reachable.add(view);
187 
188         for (Constraint constraint : view.dependedOnBy) {
189             if (vertical && !constraint.type.verticalEdge) {
190                 continue;
191             } else  if (!vertical && !constraint.type.horizontalEdge) {
192                 continue;
193             }
194 
195             assert constraint.to == view;
196             ViewData from = constraint.from;
197             if (visiting.contains(from)) {
198                 // Cycle - what do we do to highlight this?
199                 List<Constraint> path = getPathTo(start.node, view.node, vertical);
200                 if (path != null) {
201                     // TODO: display to the user somehow. We need log access for the
202                     // view rules.
203                     System.out.println(Constraint.describePath(path, null, null));
204                 }
205             } else {
206                 findBackwards(from, visiting, reachable, vertical, start);
207             }
208         }
209 
210         visiting.remove(view);
211     }
212 
getPathTo(INode from, INode to, boolean vertical)213     public List<Constraint> getPathTo(INode from, INode to, boolean vertical) {
214         // Traverse the graph of constraints and determine all nodes affected by
215         // this node
216         Set<ViewData> visiting = new HashSet<ViewData>();
217         List<Constraint> path = new ArrayList<Constraint>();
218         ViewData view = mNodeToView.get(from);
219         if (view != null) {
220             return findForwards(view, visiting, path, vertical, to);
221         }
222 
223         return null;
224     }
225 
findForwards(ViewData view, Set<ViewData> visiting, List<Constraint> path, boolean vertical, INode target)226     private List<Constraint> findForwards(ViewData view, Set<ViewData> visiting,
227             List<Constraint> path, boolean vertical, INode target) {
228         visiting.add(view);
229 
230         for (Constraint constraint : view.dependsOn) {
231             if (vertical && !constraint.type.verticalEdge) {
232                 continue;
233             } else  if (!vertical && !constraint.type.horizontalEdge) {
234                 continue;
235             }
236 
237             try {
238                 path.add(constraint);
239 
240                 if (constraint.to.node == target) {
241                     return new ArrayList<Constraint>(path);
242                 }
243 
244                 assert constraint.from == view;
245                 ViewData to = constraint.to;
246                 if (visiting.contains(to)) {
247                     // CYCLE!
248                     continue;
249                 }
250 
251                 List<Constraint> chain = findForwards(to, visiting, path, vertical, target);
252                 if (chain != null) {
253                     return chain;
254                 }
255             } finally {
256                 path.remove(constraint);
257             }
258         }
259 
260         visiting.remove(view);
261 
262         return null;
263     }
264 
265     /**
266      * Info about a specific widget child of a relative layout and its constraints. This
267      * is a node in the dependency graph.
268      */
269     static class ViewData {
270         public final INode node;
271         public final String id;
272         public final List<Constraint> dependsOn = new ArrayList<Constraint>(4);
273         public final List<Constraint> dependedOnBy = new ArrayList<Constraint>(8);
274 
ViewData(INode node, String id)275         ViewData(INode node, String id) {
276             this.node = node;
277             this.id = id;
278         }
279     }
280 
281     /**
282      * Info about a specific constraint between two widgets in a relative layout. This is
283      * an edge in the dependency graph.
284      */
285     static class Constraint {
286         public final ConstraintType type;
287         public final ViewData from;
288         public final ViewData to;
289 
290         // TODO: Initialize depth -- should be computed independently for top, left, etc.
291         // We can use this in GuidelineHandler.MatchComparator to prefer matches that
292         // are closer to a parent edge:
293         //public int depth;
294 
Constraint(ConstraintType type, ViewData from, ViewData to)295         Constraint(ConstraintType type, ViewData from, ViewData to) {
296             this.type = type;
297             this.from = from;
298             this.to = to;
299         }
300 
describePath(List<Constraint> path, String newName, String newId)301         static String describePath(List<Constraint> path, String newName, String newId) {
302             String s = "";
303             for (int i = path.size() - 1; i >= 0; i--) {
304                 Constraint constraint = path.get(i);
305                 String suffix = (i == path.size() -1) ? constraint.to.id : s;
306                 s = String.format(DEPENDENCY_FORMAT, constraint.from.id,
307                         stripLayoutAttributePrefix(constraint.type.name), suffix);
308             }
309 
310             if (newName != null) {
311                 s = String.format(DEPENDENCY_FORMAT, s, stripLayoutAttributePrefix(newName),
312                         BaseLayoutRule.stripIdPrefix(newId));
313             }
314 
315             return s;
316         }
317 
stripLayoutAttributePrefix(String name)318         private static String stripLayoutAttributePrefix(String name) {
319             if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
320                 return name.substring(ATTR_LAYOUT_RESOURCE_PREFIX.length());
321             }
322 
323             return name;
324         }
325     }
326 }
327