blob: 26bd35a1476fce4d002b5d2cba98d15f4cb7d3c1 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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.intellij.android.designer.model.layout.relative;
import com.android.tools.lint.detector.api.LintUtils;
import com.intellij.android.designer.model.RadViewComponent;
import com.intellij.designer.model.RadComponent;
import com.intellij.psi.xml.XmlAttribute;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.VALUE_TRUE;
/**
* 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>
*/
public class DependencyGraph {
@NonNls private static final String KEY = "DependencyGraph";
/**
* 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<RadViewComponent, ViewData> myNodeToView = new HashMap<RadViewComponent, ViewData>();
/**
* Returns the {@link DependencyGraph} for the given relative layout widget
* @param layout the relative layout
* @return a {@link DependencyGraph} for the layout
*/
@NotNull
public static DependencyGraph get(@NotNull RadViewComponent layout) {
DependencyGraph graph = layout.getClientProperty(KEY);
if (graph == null) {
graph = new DependencyGraph(layout);
layout.setClientProperty(KEY, graph);
}
return graph;
}
/**
* Ensures that the dependency graph for the given layout is refreshed (if it is cached)
*
* @param layout the relative layout
*/
public static void refresh(@NotNull RadViewComponent layout) {
layout.extractClientProperty(KEY);
}
/**
* Constructs a new {@link DependencyGraph} for the given relative layout
*/
private DependencyGraph(RadViewComponent layout) {
List<RadComponent> nodes = layout.getChildren();
// Parent view:
String parentId = layout.getId();
if (parentId != null) {
parentId = LintUtils.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);
myNodeToView.put(layout, parentView);
Map<String, ViewData> idToView = new HashMap<String, ViewData>();
idToView.put(parentId, parentView);
for (RadViewComponent child : RadViewComponent.getViewComponents(nodes)) {
String id = child.getId();
if (id != null) {
id = LintUtils.stripIdPrefix(id);
}
ViewData view = new ViewData(child, id);
myNodeToView.put(child, view);
if (id != null) {
idToView.put(id, view);
}
}
for (ViewData view : myNodeToView.values()) {
for (XmlAttribute attribute : view.node.getTag().getAttributes()) {
String name = attribute.getLocalName();
ConstraintType type = ConstraintType.fromAttribute(name);
if (type != null) {
String value = attribute.getValue();
if (type.targetParent) {
if (VALUE_TRUE.equals(value)) {
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 = LintUtils.stripIdPrefix(value);
ViewData target = idToView.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(RadViewComponent node) {
return myNodeToView.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<RadViewComponent> dependsOn(Collection<? extends RadViewComponent> 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 (RadViewComponent node : nodes) {
ViewData view = myNodeToView.get(node);
if (view != null) {
findBackwards(view, visiting, reachable, vertical, view);
}
}
Set<RadViewComponent> dependents = new HashSet<RadViewComponent>(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);
}
@Nullable
public List<Constraint> getPathTo(RadViewComponent from, RadViewComponent 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 = myNodeToView.get(from);
if (view != null) {
return findForwards(view, visiting, path, vertical, to);
}
return null;
}
@Nullable
private static List<Constraint> findForwards(ViewData view,
Set<ViewData> visiting,
List<Constraint> path,
boolean vertical,
RadViewComponent 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 {
@NotNull public final RadViewComponent node;
@Nullable public final String id;
@NotNull public final List<Constraint> dependsOn = new ArrayList<Constraint>(4);
@NotNull public final List<Constraint> dependedOnBy = new ArrayList<Constraint>(8);
ViewData(@NotNull RadViewComponent node, @Nullable 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 {
@NotNull 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(@NotNull ConstraintType type, @NotNull ViewData from, @NotNull ViewData to) {
this.type = type;
this.from = from;
this.to = to;
}
static String describePath(@NotNull List<Constraint> path, @Nullable String newName, @Nullable 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), newId != null ? LintUtils.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;
}
}
}