aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ConstraintPainter.java783
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ConstraintType.java241
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DeletionHandler.java267
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java326
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java839
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelinePainter.java208
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/Match.java100
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/MoveHandler.java299
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ResizeHandler.java265
9 files changed, 3328 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ConstraintPainter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ConstraintPainter.java
new file mode 100644
index 000000000..447d2d880
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ConstraintPainter.java
@@ -0,0 +1,783 @@
+/*
+ * 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.ide.common.api.DrawingStyle.DEPENDENCY;
+import static com.android.ide.common.api.DrawingStyle.GUIDELINE;
+import static com.android.ide.common.api.DrawingStyle.GUIDELINE_DASHED;
+import static com.android.ide.common.api.SegmentType.BASELINE;
+import static com.android.ide.common.api.SegmentType.BOTTOM;
+import static com.android.ide.common.api.SegmentType.CENTER_HORIZONTAL;
+import static com.android.ide.common.api.SegmentType.CENTER_VERTICAL;
+import static com.android.ide.common.api.SegmentType.LEFT;
+import static com.android.ide.common.api.SegmentType.RIGHT;
+import static com.android.ide.common.api.SegmentType.TOP;
+import static com.android.ide.common.api.SegmentType.UNKNOWN;
+import static com.android.ide.common.layout.relative.ConstraintType.ALIGN_BASELINE;
+import static com.android.ide.common.layout.relative.ConstraintType.ALIGN_BOTTOM;
+import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_ABOVE;
+import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_BELOW;
+import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_LEFT_OF;
+import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_RIGHT_OF;
+
+import com.android.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.IGraphics;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.common.layout.relative.DependencyGraph.Constraint;
+import com.android.ide.common.layout.relative.DependencyGraph.ViewData;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The {@link ConstraintPainter} is responsible for painting relative layout constraints -
+ * such as a source node having its top edge constrained to a target node with a given margin.
+ * This painter is used both to show static constraints, as well as visualizing proposed
+ * constraints during a move or resize operation.
+ */
+public class ConstraintPainter {
+ /** The size of the arrow head */
+ private static final int ARROW_SIZE = 5;
+ /** Size (height for horizontal, and width for vertical) parent feedback rectangles */
+ private static final int PARENT_RECT_SIZE = 12;
+
+ /**
+ * Paints a given match as a constraint.
+ *
+ * @param graphics the graphics context
+ * @param sourceBounds the source bounds
+ * @param match the match
+ */
+ static void paintConstraint(IGraphics graphics, Rect sourceBounds, Match match) {
+ Rect targetBounds = match.edge.node.getBounds();
+ ConstraintType type = match.type;
+ assert type != null;
+ paintConstraint(graphics, type, match.with.node, sourceBounds, match.edge.node,
+ targetBounds, null /* allConstraints */, true /* highlightTargetEdge */);
+ }
+
+ /**
+ * Paints a constraint.
+ * <p>
+ * TODO: when there are multiple links originating in the same direction from
+ * center, maybe offset them slightly from each other?
+ *
+ * @param graphics the graphics context to draw into
+ * @param constraint The constraint to be drawn
+ */
+ private static void paintConstraint(IGraphics graphics, Constraint constraint,
+ Set<Constraint> allConstraints) {
+ ViewData source = constraint.from;
+ ViewData target = constraint.to;
+
+ INode sourceNode = source.node;
+ INode targetNode = target.node;
+ if (sourceNode == targetNode) {
+ // Self reference - don't visualize
+ return;
+ }
+
+ Rect sourceBounds = sourceNode.getBounds();
+ Rect targetBounds = targetNode.getBounds();
+ paintConstraint(graphics, constraint.type, sourceNode, sourceBounds, targetNode,
+ targetBounds, allConstraints, false /* highlightTargetEdge */);
+ }
+
+ /**
+ * Paint selection feedback by painting constraints for the selected nodes
+ *
+ * @param graphics the graphics context
+ * @param parentNode the parent relative layout
+ * @param childNodes the nodes whose constraints should be painted
+ * @param showDependents whether incoming constraints should be shown as well
+ */
+ public static void paintSelectionFeedback(IGraphics graphics, INode parentNode,
+ List<? extends INode> childNodes, boolean showDependents) {
+
+ DependencyGraph dependencyGraph = new DependencyGraph(parentNode);
+ Set<INode> horizontalDeps = dependencyGraph.dependsOn(childNodes, false /* vertical */);
+ Set<INode> verticalDeps = dependencyGraph.dependsOn(childNodes, true /* vertical */);
+ Set<INode> deps = new HashSet<INode>(horizontalDeps.size() + verticalDeps.size());
+ deps.addAll(horizontalDeps);
+ deps.addAll(verticalDeps);
+ if (deps.size() > 0) {
+ graphics.useStyle(DEPENDENCY);
+ for (INode node : deps) {
+ // Don't highlight the selected nodes themselves
+ if (childNodes.contains(node)) {
+ continue;
+ }
+ Rect bounds = node.getBounds();
+ graphics.fillRect(bounds);
+ }
+ }
+
+ graphics.useStyle(GUIDELINE);
+ for (INode childNode : childNodes) {
+ ViewData view = dependencyGraph.getView(childNode);
+ if (view == null) {
+ continue;
+ }
+
+ // Paint all incoming constraints
+ if (showDependents) {
+ paintConstraints(graphics, view.dependedOnBy);
+ }
+
+ // Paint all outgoing constraints
+ paintConstraints(graphics, view.dependsOn);
+ }
+ }
+
+ /**
+ * Paints a set of constraints.
+ */
+ private static void paintConstraints(IGraphics graphics, List<Constraint> constraints) {
+ Set<Constraint> mutableConstraintSet = new HashSet<Constraint>(constraints);
+
+ // WORKAROUND! Hide alignBottom attachments if we also have a alignBaseline
+ // constraint; this is because we also *add* alignBottom attachments when you add
+ // alignBaseline constraints to work around a surprising behavior of baseline
+ // constraints.
+ for (Constraint constraint : constraints) {
+ if (constraint.type == ALIGN_BASELINE) {
+ // Remove any baseline
+ for (Constraint c : constraints) {
+ if (c.type == ALIGN_BOTTOM && c.to.node == constraint.to.node) {
+ mutableConstraintSet.remove(c);
+ }
+ }
+ }
+ }
+
+ for (Constraint constraint : constraints) {
+ // paintConstraint can digest more than one constraint, so we need to keep
+ // checking to see if the given constraint is still relevant.
+ if (mutableConstraintSet.contains(constraint)) {
+ paintConstraint(graphics, constraint, mutableConstraintSet);
+ }
+ }
+ }
+
+ /**
+ * Paints a constraint of the given type from the given source node, to the
+ * given target node, with the specified bounds.
+ */
+ private static void paintConstraint(IGraphics graphics, ConstraintType type, INode sourceNode,
+ Rect sourceBounds, INode targetNode, Rect targetBounds,
+ Set<Constraint> allConstraints, boolean highlightTargetEdge) {
+
+ SegmentType sourceSegmentTypeX = type.sourceSegmentTypeX;
+ SegmentType sourceSegmentTypeY = type.sourceSegmentTypeY;
+ SegmentType targetSegmentTypeX = type.targetSegmentTypeX;
+ SegmentType targetSegmentTypeY = type.targetSegmentTypeY;
+
+ // Horizontal center constraint?
+ if (sourceSegmentTypeX == CENTER_VERTICAL && targetSegmentTypeX == CENTER_VERTICAL) {
+ paintHorizontalCenterConstraint(graphics, sourceBounds, targetBounds);
+ return;
+ }
+
+ // Vertical center constraint?
+ if (sourceSegmentTypeY == CENTER_HORIZONTAL && targetSegmentTypeY == CENTER_HORIZONTAL) {
+ paintVerticalCenterConstraint(graphics, sourceBounds, targetBounds);
+ return;
+ }
+
+ // Corner constraint?
+ if (allConstraints != null
+ && (type == LAYOUT_ABOVE || type == LAYOUT_BELOW
+ || type == LAYOUT_LEFT_OF || type == LAYOUT_RIGHT_OF)) {
+ if (paintCornerConstraint(graphics, type, sourceNode, sourceBounds, targetNode,
+ targetBounds, allConstraints)) {
+ return;
+ }
+ }
+
+ // Vertical constraint?
+ if (sourceSegmentTypeX == UNKNOWN) {
+ paintVerticalConstraint(graphics, type, sourceNode, sourceBounds, targetNode,
+ targetBounds, highlightTargetEdge);
+ return;
+ }
+
+ // Horizontal constraint?
+ if (sourceSegmentTypeY == UNKNOWN) {
+ paintHorizontalConstraint(graphics, type, sourceNode, sourceBounds, targetNode,
+ targetBounds, highlightTargetEdge);
+ return;
+ }
+
+ // This shouldn't happen - it means we have a constraint that defines all sides
+ // and is not a centering constraint
+ assert false;
+ }
+
+ /**
+ * Paints a corner constraint, or returns false if this constraint is not a corner.
+ * A corner is one where there are two constraints from this source node to the
+ * same target node, one horizontal and one vertical, to the closest edges on
+ * the target node.
+ * <p>
+ * Corners are a common occurrence. If we treat the horizontal and vertical
+ * constraints separately (below & toRightOf), then we end up with a lot of
+ * extra lines and arrows -- e.g. two shared edges and arrows pointing to these
+ * shared edges:
+ *
+ * <pre>
+ * +--------+ |
+ * | Target -->
+ * +----|---+ |
+ * v
+ * - - - - - -|- - - - - -
+ * ^
+ * | +---|----+
+ * <-- Source |
+ * | +--------+
+ *
+ * Instead, we can simply draw a diagonal arrow here to represent BOTH constraints and
+ * reduce clutter:
+ *
+ * +---------+
+ * | Target _|
+ * +-------|\+
+ * \
+ * \--------+
+ * | Source |
+ * +--------+
+ * </pre>
+ *
+ * @param graphics the graphics context to draw
+ * @param type the constraint to be drawn
+ * @param sourceNode the source node
+ * @param sourceBounds the bounds of the source node
+ * @param targetNode the target node
+ * @param targetBounds the bounds of the target node
+ * @param allConstraints the set of all constraints; if a corner is found and painted the
+ * matching corner constraint is removed from the set
+ * @return true if the constraint was handled and painted as a corner, false otherwise
+ */
+ private static boolean paintCornerConstraint(IGraphics graphics, ConstraintType type,
+ INode sourceNode, Rect sourceBounds, INode targetNode, Rect targetBounds,
+ Set<Constraint> allConstraints) {
+
+ SegmentType sourceSegmentTypeX = type.sourceSegmentTypeX;
+ SegmentType sourceSegmentTypeY = type.sourceSegmentTypeY;
+ SegmentType targetSegmentTypeX = type.targetSegmentTypeX;
+ SegmentType targetSegmentTypeY = type.targetSegmentTypeY;
+
+ ConstraintType opposite1 = null, opposite2 = null;
+ switch (type) {
+ case LAYOUT_BELOW:
+ case LAYOUT_ABOVE:
+ opposite1 = LAYOUT_LEFT_OF;
+ opposite2 = LAYOUT_RIGHT_OF;
+ break;
+ case LAYOUT_LEFT_OF:
+ case LAYOUT_RIGHT_OF:
+ opposite1 = LAYOUT_ABOVE;
+ opposite2 = LAYOUT_BELOW;
+ break;
+ default:
+ return false;
+ }
+ Constraint pair = null;
+ for (Constraint constraint : allConstraints) {
+ if ((constraint.type == opposite1 || constraint.type == opposite2) &&
+ constraint.to.node == targetNode && constraint.from.node == sourceNode) {
+ pair = constraint;
+ break;
+ }
+ }
+
+ // TODO -- ensure that the nodes are adjacent! In other words, that
+ // their bounds are within N pixels.
+
+ if (pair != null) {
+ // Visualize the corner constraint
+ if (sourceSegmentTypeX == UNKNOWN) {
+ sourceSegmentTypeX = pair.type.sourceSegmentTypeX;
+ }
+ if (sourceSegmentTypeY == UNKNOWN) {
+ sourceSegmentTypeY = pair.type.sourceSegmentTypeY;
+ }
+ if (targetSegmentTypeX == UNKNOWN) {
+ targetSegmentTypeX = pair.type.targetSegmentTypeX;
+ }
+ if (targetSegmentTypeY == UNKNOWN) {
+ targetSegmentTypeY = pair.type.targetSegmentTypeY;
+ }
+
+ int x1, y1, x2, y2;
+ if (sourceSegmentTypeX == LEFT) {
+ x1 = sourceBounds.x + 1 * sourceBounds.w / 4;
+ } else {
+ x1 = sourceBounds.x + 3 * sourceBounds.w / 4;
+ }
+ if (sourceSegmentTypeY == TOP) {
+ y1 = sourceBounds.y + 1 * sourceBounds.h / 4;
+ } else {
+ y1 = sourceBounds.y + 3 * sourceBounds.h / 4;
+ }
+ if (targetSegmentTypeX == LEFT) {
+ x2 = targetBounds.x + 1 * targetBounds.w / 4;
+ } else {
+ x2 = targetBounds.x + 3 * targetBounds.w / 4;
+ }
+ if (targetSegmentTypeY == TOP) {
+ y2 = targetBounds.y + 1 * targetBounds.h / 4;
+ } else {
+ y2 = targetBounds.y + 3 * targetBounds.h / 4;
+ }
+
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(x1, y1, x2, y2, ARROW_SIZE);
+
+ // Don't process this constraint on its own later.
+ allConstraints.remove(pair);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Paints a vertical constraint, handling the various scenarios where there are
+ * margins, or where the two nodes overlap horizontally and where they don't, etc.
+ * <p>
+ * Here's an example of what will be shown for a "below" constraint where the
+ * nodes do not overlap horizontally and the target node has a bottom margin:
+ * <pre>
+ * +--------+
+ * | Target |
+ * +--------+
+ * |
+ * v
+ * - - - - - - - - - - - - - -
+ * ^
+ * |
+ * +--------+
+ * | Source |
+ * +--------+
+ * </pre>
+ */
+ private static void paintVerticalConstraint(IGraphics graphics, ConstraintType type,
+ INode sourceNode, Rect sourceBounds, INode targetNode, Rect targetBounds,
+ boolean highlightTargetEdge) {
+ SegmentType sourceSegmentTypeY = type.sourceSegmentTypeY;
+ SegmentType targetSegmentTypeY = type.targetSegmentTypeY;
+ Margins targetMargins = targetNode.getMargins();
+
+ assert sourceSegmentTypeY != UNKNOWN;
+ assert targetBounds != null;
+
+ int sourceY = sourceSegmentTypeY.getY(sourceNode, sourceBounds);
+ int targetY = targetSegmentTypeY ==
+ UNKNOWN ? sourceY : targetSegmentTypeY.getY(targetNode, targetBounds);
+
+ if (highlightTargetEdge && type.isRelativeToParentEdge()) {
+ graphics.useStyle(DrawingStyle.DROP_ZONE_ACTIVE);
+ graphics.fillRect(targetBounds.x, targetY - PARENT_RECT_SIZE / 2,
+ targetBounds.x2(), targetY + PARENT_RECT_SIZE / 2);
+ }
+
+ // First see if the two views overlap horizontally. If so, we can just draw a direct
+ // arrow from the source up to (or down to) the target.
+ //
+ // +--------+
+ // | Target |
+ // +--------+
+ // ^
+ // |
+ // |
+ // +--------+
+ // | Source |
+ // +--------+
+ //
+ int maxLeft = Math.max(sourceBounds.x, targetBounds.x);
+ int minRight = Math.min(sourceBounds.x2(), targetBounds.x2());
+
+ int center = (maxLeft + minRight) / 2;
+ if (center > sourceBounds.x && center < sourceBounds.x2()) {
+ // Yes, the lines overlap -- just draw a straight arrow
+ //
+ //
+ // If however there is a margin on the target edge, it should be drawn like this:
+ //
+ // +--------+
+ // | Target |
+ // +--------+
+ // |
+ // |
+ // v
+ // - - - - - - -
+ // ^
+ // |
+ // |
+ // +--------+
+ // | Source |
+ // +--------+
+ //
+ // Use a minimum threshold for this visualization since it doesn't look good
+ // for small margins
+ if (targetSegmentTypeY == BOTTOM && targetMargins.bottom > 5) {
+ int sharedY = targetY + targetMargins.bottom;
+ if (sourceY > sharedY + 2) { // Skip when source falls on the margin line
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(targetBounds.x, sharedY, targetBounds.x2(), sharedY);
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(center, sourceY, center, sharedY + 2, ARROW_SIZE);
+ graphics.drawArrow(center, targetY, center, sharedY - 3, ARROW_SIZE);
+ } else {
+ graphics.useStyle(GUIDELINE);
+ // Draw reverse arrow to make it clear the node is as close
+ // at it can be
+ graphics.drawArrow(center, targetY, center, sourceY, ARROW_SIZE);
+ }
+ return;
+ } else if (targetSegmentTypeY == TOP && targetMargins.top > 5) {
+ int sharedY = targetY - targetMargins.top;
+ if (sourceY < sharedY - 2) {
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(targetBounds.x, sharedY, targetBounds.x2(), sharedY);
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(center, sourceY, center, sharedY - 3, ARROW_SIZE);
+ graphics.drawArrow(center, targetY, center, sharedY + 3, ARROW_SIZE);
+ } else {
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(center, targetY, center, sourceY, ARROW_SIZE);
+ }
+ return;
+ }
+
+ // TODO: If the center falls smack in the center of the sourceBounds,
+ // AND the source node is part of the selection, then adjust the
+ // center location such that it is off to the side, let's say 1/4 or 3/4 of
+ // the overlap region, to ensure that it does not overlap the center selection
+ // handle
+
+ // When the constraint is for two immediately adjacent edges, we
+ // need to make some adjustments to make sure the arrow points in the right
+ // direction
+ if (sourceY == targetY) {
+ if (sourceSegmentTypeY == BOTTOM || sourceSegmentTypeY == BASELINE) {
+ sourceY -= 2 * ARROW_SIZE;
+ } else if (sourceSegmentTypeY == TOP) {
+ sourceY += 2 * ARROW_SIZE;
+ } else {
+ assert sourceSegmentTypeY == CENTER_HORIZONTAL : sourceSegmentTypeY;
+ sourceY += sourceBounds.h / 2 - 2 * ARROW_SIZE;
+ }
+ } else if (sourceSegmentTypeY == BASELINE) {
+ sourceY = targetY - 2 * ARROW_SIZE;
+ }
+
+ // Center the vertical line in the overlap region
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(center, sourceY, center, targetY, ARROW_SIZE);
+
+ return;
+ }
+
+ // If there is no horizontal overlap in the vertical constraints, then we
+ // will show the attachment relative to a dashed line that extends beyond
+ // the target bounds, like this:
+ //
+ // +--------+
+ // | Target |
+ // +--------+ - - - - - - - - -
+ // ^
+ // |
+ // +--------+
+ // | Source |
+ // +--------+
+ //
+ // However, if the target node has a vertical margin, we may need to offset
+ // the line:
+ //
+ // +--------+
+ // | Target |
+ // +--------+
+ // |
+ // v
+ // - - - - - - - - - - - - - -
+ // ^
+ // |
+ // +--------+
+ // | Source |
+ // +--------+
+ //
+ // If not, we'll need to indicate a shared edge. This is the edge that separate
+ // them (but this will require me to evaluate margins!)
+
+ // Compute overlap region and pick the middle
+ int sharedY = targetSegmentTypeY ==
+ UNKNOWN ? sourceY : targetSegmentTypeY.getY(targetNode, targetBounds);
+ if (type.relativeToMargin) {
+ if (targetSegmentTypeY == TOP) {
+ sharedY -= targetMargins.top;
+ } else if (targetSegmentTypeY == BOTTOM) {
+ sharedY += targetMargins.bottom;
+ }
+ }
+
+ int startX;
+ int endX;
+ if (center <= sourceBounds.x) {
+ startX = targetBounds.x + targetBounds.w / 4;
+ endX = sourceBounds.x2();
+ } else {
+ assert (center >= sourceBounds.x2());
+ startX = sourceBounds.x;
+ endX = targetBounds.x + 3 * targetBounds.w / 4;
+ }
+ // Must draw segmented line instead
+ // Place the arrow 1/4 instead of 1/2 in the source to avoid overlapping with the
+ // selection handles
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(startX, sharedY, endX, sharedY);
+
+ // Adjust position of source arrow such that it does not sit across edge; it
+ // should point directly at the edge
+ if (Math.abs(sharedY - sourceY) < 2 * ARROW_SIZE) {
+ if (sourceSegmentTypeY == BASELINE) {
+ sourceY = sharedY - 2 * ARROW_SIZE;
+ } else if (sourceSegmentTypeY == TOP) {
+ sharedY = sourceY;
+ sourceY = sharedY + 2 * ARROW_SIZE;
+ } else {
+ sharedY = sourceY;
+ sourceY = sharedY - 2 * ARROW_SIZE;
+ }
+ }
+
+ graphics.useStyle(GUIDELINE);
+
+ // Draw the line from the source anchor to the shared edge
+ int x = sourceBounds.x + ((sourceSegmentTypeY == BASELINE) ?
+ sourceBounds.w / 2 : sourceBounds.w / 4);
+ graphics.drawArrow(x, sourceY, x, sharedY, ARROW_SIZE);
+
+ // Draw the line from the target to the horizontal shared edge
+ int tx = targetBounds.centerX();
+ if (targetSegmentTypeY == TOP) {
+ int ty = targetBounds.y;
+ int margin = targetMargins.top;
+ if (margin == 0 || !type.relativeToMargin) {
+ graphics.drawArrow(tx, ty + 2 * ARROW_SIZE, tx, ty, ARROW_SIZE);
+ } else {
+ graphics.drawArrow(tx, ty, tx, ty - margin, ARROW_SIZE);
+ }
+ } else if (targetSegmentTypeY == BOTTOM) {
+ int ty = targetBounds.y2();
+ int margin = targetMargins.bottom;
+ if (margin == 0 || !type.relativeToMargin) {
+ graphics.drawArrow(tx, ty - 2 * ARROW_SIZE, tx, ty, ARROW_SIZE);
+ } else {
+ graphics.drawArrow(tx, ty, tx, ty + margin, ARROW_SIZE);
+ }
+ } else {
+ assert targetSegmentTypeY == BASELINE : targetSegmentTypeY;
+ int ty = targetSegmentTypeY.getY(targetNode, targetBounds);
+ graphics.drawArrow(tx, ty - 2 * ARROW_SIZE, tx, ty, ARROW_SIZE);
+ }
+
+ return;
+ }
+
+ /**
+ * Paints a horizontal constraint, handling the various scenarios where there are margins,
+ * or where the two nodes overlap horizontally and where they don't, etc.
+ */
+ private static void paintHorizontalConstraint(IGraphics graphics, ConstraintType type,
+ INode sourceNode, Rect sourceBounds, INode targetNode, Rect targetBounds,
+ boolean highlightTargetEdge) {
+ SegmentType sourceSegmentTypeX = type.sourceSegmentTypeX;
+ SegmentType targetSegmentTypeX = type.targetSegmentTypeX;
+ Margins targetMargins = targetNode.getMargins();
+
+ assert sourceSegmentTypeX != UNKNOWN;
+ assert targetBounds != null;
+
+ // See paintVerticalConstraint for explanations of the various cases.
+
+ int sourceX = sourceSegmentTypeX.getX(sourceNode, sourceBounds);
+ int targetX = targetSegmentTypeX == UNKNOWN ?
+ sourceX : targetSegmentTypeX.getX(targetNode, targetBounds);
+
+ if (highlightTargetEdge && type.isRelativeToParentEdge()) {
+ graphics.useStyle(DrawingStyle.DROP_ZONE_ACTIVE);
+ graphics.fillRect(targetX - PARENT_RECT_SIZE / 2, targetBounds.y,
+ targetX + PARENT_RECT_SIZE / 2, targetBounds.y2());
+ }
+
+ int maxTop = Math.max(sourceBounds.y, targetBounds.y);
+ int minBottom = Math.min(sourceBounds.y2(), targetBounds.y2());
+
+ // First see if the two views overlap vertically. If so, we can just draw a direct
+ // arrow from the source over to the target.
+ int center = (maxTop + minBottom) / 2;
+ if (center > sourceBounds.y && center < sourceBounds.y2()) {
+ // See if we should draw a margin line
+ if (targetSegmentTypeX == RIGHT && targetMargins.right > 5) {
+ int sharedX = targetX + targetMargins.right;
+ if (sourceX > sharedX + 2) { // Skip when source falls on the margin line
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(sharedX, targetBounds.y, sharedX, targetBounds.y2());
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(sourceX, center, sharedX + 2, center, ARROW_SIZE);
+ graphics.drawArrow(targetX, center, sharedX - 3, center, ARROW_SIZE);
+ } else {
+ graphics.useStyle(GUIDELINE);
+ // Draw reverse arrow to make it clear the node is as close
+ // at it can be
+ graphics.drawArrow(targetX, center, sourceX, center, ARROW_SIZE);
+ }
+ return;
+ } else if (targetSegmentTypeX == LEFT && targetMargins.left > 5) {
+ int sharedX = targetX - targetMargins.left;
+ if (sourceX < sharedX - 2) {
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(sharedX, targetBounds.y, sharedX, targetBounds.y2());
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(sourceX, center, sharedX - 3, center, ARROW_SIZE);
+ graphics.drawArrow(targetX, center, sharedX + 3, center, ARROW_SIZE);
+ } else {
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(targetX, center, sourceX, center, ARROW_SIZE);
+ }
+ return;
+ }
+
+ if (sourceX == targetX) {
+ if (sourceSegmentTypeX == RIGHT) {
+ sourceX -= 2 * ARROW_SIZE;
+ } else if (sourceSegmentTypeX == LEFT ) {
+ sourceX += 2 * ARROW_SIZE;
+ } else {
+ assert sourceSegmentTypeX == CENTER_VERTICAL : sourceSegmentTypeX;
+ sourceX += sourceBounds.w / 2 - 2 * ARROW_SIZE;
+ }
+ }
+
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(sourceX, center, targetX, center, ARROW_SIZE);
+ return;
+ }
+
+ // Segment line
+
+ // Compute overlap region and pick the middle
+ int sharedX = targetSegmentTypeX == UNKNOWN ?
+ sourceX : targetSegmentTypeX.getX(targetNode, targetBounds);
+ if (type.relativeToMargin) {
+ if (targetSegmentTypeX == LEFT) {
+ sharedX -= targetMargins.left;
+ } else if (targetSegmentTypeX == RIGHT) {
+ sharedX += targetMargins.right;
+ }
+ }
+
+ int startY, endY;
+ if (center <= sourceBounds.y) {
+ startY = targetBounds.y + targetBounds.h / 4;
+ endY = sourceBounds.y2();
+ } else {
+ assert (center >= sourceBounds.y2());
+ startY = sourceBounds.y;
+ endY = targetBounds.y + 3 * targetBounds.h / 2;
+ }
+
+ // Must draw segmented line instead
+ // Place at 1/4 instead of 1/2 to avoid overlapping with selection handles
+ int y = sourceBounds.y + sourceBounds.h / 4;
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(sharedX, startY, sharedX, endY);
+
+ // Adjust position of source arrow such that it does not sit across edge; it
+ // should point directly at the edge
+ if (Math.abs(sharedX - sourceX) < 2 * ARROW_SIZE) {
+ if (sourceSegmentTypeX == LEFT) {
+ sharedX = sourceX;
+ sourceX = sharedX + 2 * ARROW_SIZE;
+ } else {
+ sharedX = sourceX;
+ sourceX = sharedX - 2 * ARROW_SIZE;
+ }
+ }
+
+ graphics.useStyle(GUIDELINE);
+
+ // Draw the line from the source anchor to the shared edge
+ graphics.drawArrow(sourceX, y, sharedX, y, ARROW_SIZE);
+
+ // Draw the line from the target to the horizontal shared edge
+ int ty = targetBounds.centerY();
+ if (targetSegmentTypeX == LEFT) {
+ int tx = targetBounds.x;
+ int margin = targetMargins.left;
+ if (margin == 0 || !type.relativeToMargin) {
+ graphics.drawArrow(tx + 2 * ARROW_SIZE, ty, tx, ty, ARROW_SIZE);
+ } else {
+ graphics.drawArrow(tx, ty, tx - margin, ty, ARROW_SIZE);
+ }
+ } else {
+ assert targetSegmentTypeX == RIGHT;
+ int tx = targetBounds.x2();
+ int margin = targetMargins.right;
+ if (margin == 0 || !type.relativeToMargin) {
+ graphics.drawArrow(tx - 2 * ARROW_SIZE, ty, tx, ty, ARROW_SIZE);
+ } else {
+ graphics.drawArrow(tx, ty, tx + margin, ty, ARROW_SIZE);
+ }
+ }
+
+ return;
+ }
+
+ /**
+ * Paints a vertical center constraint. The constraint is shown as a dashed line
+ * through the vertical view, and a solid line over the node bounds.
+ */
+ private static void paintVerticalCenterConstraint(IGraphics graphics, Rect sourceBounds,
+ Rect targetBounds) {
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(targetBounds.x, targetBounds.centerY(),
+ targetBounds.x2(), targetBounds.centerY());
+ graphics.useStyle(GUIDELINE);
+ graphics.drawLine(sourceBounds.x, sourceBounds.centerY(),
+ sourceBounds.x2(), sourceBounds.centerY());
+ }
+
+ /**
+ * Paints a horizontal center constraint. The constraint is shown as a dashed line
+ * through the horizontal view, and a solid line over the node bounds.
+ */
+ private static void paintHorizontalCenterConstraint(IGraphics graphics, Rect sourceBounds,
+ Rect targetBounds) {
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(targetBounds.centerX(), targetBounds.y,
+ targetBounds.centerX(), targetBounds.y2());
+ graphics.useStyle(GUIDELINE);
+ graphics.drawLine(sourceBounds.centerX(), sourceBounds.y,
+ sourceBounds.centerX(), sourceBounds.y2());
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ConstraintType.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ConstraintType.java
new file mode 100644
index 000000000..ed4ac1bf4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ConstraintType.java
@@ -0,0 +1,241 @@
+/*
+ * 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.ide.common.api.SegmentType.BASELINE;
+import static com.android.ide.common.api.SegmentType.BOTTOM;
+import static com.android.ide.common.api.SegmentType.CENTER_HORIZONTAL;
+import static com.android.ide.common.api.SegmentType.CENTER_VERTICAL;
+import static com.android.ide.common.api.SegmentType.LEFT;
+import static com.android.ide.common.api.SegmentType.RIGHT;
+import static com.android.ide.common.api.SegmentType.TOP;
+import static com.android.ide.common.api.SegmentType.UNKNOWN;
+import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_IN_PARENT;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.SegmentType;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Each constraint type corresponds to a type of constraint available for the
+ * RelativeLayout; for example, {@link #LAYOUT_ABOVE} corresponds to the layout_above constraint.
+ */
+enum ConstraintType {
+ LAYOUT_ABOVE(ATTR_LAYOUT_ABOVE,
+ null /* sourceX */, BOTTOM, null /* targetX */, TOP,
+ false /* targetParent */, true /* horizontalEdge */, false /* verticalEdge */,
+ true /* relativeToMargin */),
+
+ LAYOUT_BELOW(ATTR_LAYOUT_BELOW, null, TOP, null, BOTTOM, false, true, false, true),
+ ALIGN_TOP(ATTR_LAYOUT_ALIGN_TOP, null, TOP, null, TOP, false, true, false, false),
+ ALIGN_BOTTOM(ATTR_LAYOUT_ALIGN_BOTTOM, null, BOTTOM, null, BOTTOM, false, true, false, false),
+ ALIGN_LEFT(ATTR_LAYOUT_ALIGN_LEFT, LEFT, null, LEFT, null, false, false, true, false),
+ ALIGN_RIGHT(ATTR_LAYOUT_ALIGN_RIGHT, RIGHT, null, RIGHT, null, false, false, true, false),
+ LAYOUT_LEFT_OF(ATTR_LAYOUT_TO_LEFT_OF, RIGHT, null, LEFT, null, false, false, true, true),
+ LAYOUT_RIGHT_OF(ATTR_LAYOUT_TO_RIGHT_OF, LEFT, null, RIGHT, null, false, false, true, true),
+ ALIGN_PARENT_TOP(ATTR_LAYOUT_ALIGN_PARENT_TOP, null, TOP, null, TOP, true, true, false, false),
+ ALIGN_BASELINE(ATTR_LAYOUT_ALIGN_BASELINE, null, BASELINE, null, BASELINE, false, true, false,
+ false),
+ ALIGN_PARENT_LEFT(ATTR_LAYOUT_ALIGN_PARENT_LEFT, LEFT, null, LEFT, null, true, false, true,
+ false),
+ ALIGN_PARENT_RIGHT(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, RIGHT, null, RIGHT, null, true, false, true,
+ false),
+ ALIGN_PARENT_BOTTOM(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, null, BOTTOM, null, BOTTOM, true, true,
+ false, false),
+ LAYOUT_CENTER_HORIZONTAL(ATTR_LAYOUT_CENTER_HORIZONTAL, CENTER_VERTICAL, null, CENTER_VERTICAL,
+ null, true, true, false, false),
+ LAYOUT_CENTER_VERTICAL(ATTR_LAYOUT_CENTER_VERTICAL, null, CENTER_HORIZONTAL, null,
+ CENTER_HORIZONTAL, true, false, true, false),
+ LAYOUT_CENTER_IN_PARENT(ATTR_LAYOUT_CENTER_IN_PARENT, CENTER_VERTICAL, CENTER_HORIZONTAL,
+ CENTER_VERTICAL, CENTER_HORIZONTAL, true, true, true, false);
+
+ private ConstraintType(String name, SegmentType sourceSegmentTypeX,
+ SegmentType sourceSegmentTypeY, SegmentType targetSegmentTypeX,
+ SegmentType targetSegmentTypeY, boolean targetParent, boolean horizontalEdge,
+ boolean verticalEdge, boolean relativeToMargin) {
+ assert horizontalEdge || verticalEdge;
+
+ this.name = name;
+ this.sourceSegmentTypeX = sourceSegmentTypeX != null ? sourceSegmentTypeX : UNKNOWN;
+ this.sourceSegmentTypeY = sourceSegmentTypeY != null ? sourceSegmentTypeY : UNKNOWN;
+ this.targetSegmentTypeX = targetSegmentTypeX != null ? targetSegmentTypeX : UNKNOWN;
+ this.targetSegmentTypeY = targetSegmentTypeY != null ? targetSegmentTypeY : UNKNOWN;
+ this.targetParent = targetParent;
+ this.horizontalEdge = horizontalEdge;
+ this.verticalEdge = verticalEdge;
+ this.relativeToMargin = relativeToMargin;
+ }
+
+ /** The attribute name of the constraint */
+ public final String name;
+
+ /** The horizontal position of the source of the constraint */
+ public final SegmentType sourceSegmentTypeX;
+
+ /** The vertical position of the source of the constraint */
+ public final SegmentType sourceSegmentTypeY;
+
+ /** The horizontal position of the target of the constraint */
+ public final SegmentType targetSegmentTypeX;
+
+ /** The vertical position of the target of the constraint */
+ public final SegmentType targetSegmentTypeY;
+
+ /**
+ * If true, the constraint targets the parent layout, otherwise it targets another
+ * view
+ */
+ public final boolean targetParent;
+
+ /** If true, this constraint affects the horizontal dimension */
+ public final boolean horizontalEdge;
+
+ /** If true, this constraint affects the vertical dimension */
+ public final boolean verticalEdge;
+
+ /**
+ * Whether this constraint is relative to the margin bounds of the node rather than
+ * the node's actual bounds
+ */
+ public final boolean relativeToMargin;
+
+ /** Map from attribute name to constraint type */
+ private static Map<String, ConstraintType> sNameToType;
+
+ /**
+ * Returns the {@link ConstraintType} corresponding to the given attribute name, or
+ * null if not found.
+ *
+ * @param attribute the name of the attribute to look up
+ * @return the corresponding {@link ConstraintType}
+ */
+ @Nullable
+ public static ConstraintType fromAttribute(@NonNull String attribute) {
+ if (sNameToType == null) {
+ ConstraintType[] types = ConstraintType.values();
+ Map<String, ConstraintType> map = new HashMap<String, ConstraintType>(types.length);
+ for (ConstraintType type : types) {
+ map.put(type.name, type);
+ }
+ sNameToType = map;
+ }
+ return sNameToType.get(attribute);
+ }
+
+ /**
+ * Returns true if this constraint type represents a constraint where the target edge
+ * is one of the parent edges (actual edge, not center/baseline segments)
+ *
+ * @return true if the target segment is a parent edge
+ */
+ public boolean isRelativeToParentEdge() {
+ return this == ALIGN_PARENT_LEFT || this == ALIGN_PARENT_RIGHT || this == ALIGN_PARENT_TOP
+ || this == ALIGN_PARENT_BOTTOM;
+ }
+
+ /**
+ * Returns a {@link ConstraintType} for a potential match of edges.
+ *
+ * @param withParent if true, the target is the parent
+ * @param from the source edge
+ * @param to the target edge
+ * @return a {@link ConstraintType}, or null
+ */
+ @Nullable
+ public static ConstraintType forMatch(boolean withParent, SegmentType from, SegmentType to) {
+ // Attached to parent edge?
+ if (withParent) {
+ switch (from) {
+ case TOP:
+ return ALIGN_PARENT_TOP;
+ case BOTTOM:
+ return ALIGN_PARENT_BOTTOM;
+ case LEFT:
+ return ALIGN_PARENT_LEFT;
+ case RIGHT:
+ return ALIGN_PARENT_RIGHT;
+ case CENTER_HORIZONTAL:
+ return LAYOUT_CENTER_VERTICAL;
+ case CENTER_VERTICAL:
+ return LAYOUT_CENTER_HORIZONTAL;
+ }
+
+ return null;
+ }
+
+ // Attached to some other node.
+ switch (from) {
+ case TOP:
+ switch (to) {
+ case TOP:
+ return ALIGN_TOP;
+ case BOTTOM:
+ return LAYOUT_BELOW;
+ case BASELINE:
+ return ALIGN_BASELINE;
+ }
+ break;
+ case BOTTOM:
+ switch (to) {
+ case TOP:
+ return LAYOUT_ABOVE;
+ case BOTTOM:
+ return ALIGN_BOTTOM;
+ case BASELINE:
+ return ALIGN_BASELINE;
+ }
+ break;
+ case LEFT:
+ switch (to) {
+ case LEFT:
+ return ALIGN_LEFT;
+ case RIGHT:
+ return LAYOUT_RIGHT_OF;
+ }
+ break;
+ case RIGHT:
+ switch (to) {
+ case LEFT:
+ return LAYOUT_LEFT_OF;
+ case RIGHT:
+ return ALIGN_RIGHT;
+ }
+ break;
+ case BASELINE:
+ return ALIGN_BASELINE;
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DeletionHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DeletionHandler.java
new file mode 100644
index 000000000..3eac510df
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DeletionHandler.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2012 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.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.ide.common.layout.BaseViewRule.stripIdPrefix;
+import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_CENTER_HORIZONTAL;
+import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_CENTER_VERTICAL;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.INode.IAttribute;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Handles deletions in a relative layout, transferring constraints across
+ * deleted nodes
+ * <p>
+ * TODO: Consider adding the
+ * {@link SdkConstants#ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING} attribute to a
+ * node if it's pointing to a node which is deleted and which has no transitive
+ * reference to another node.
+ */
+public class DeletionHandler {
+ private final INode mLayout;
+ private final INode[] mChildren;
+ private final List<INode> mDeleted;
+ private final Set<String> mDeletedIds;
+ private final Map<String, INode> mNodeMap;
+ private final List<INode> mMoved;
+
+ /**
+ * Creates a new {@link DeletionHandler}
+ *
+ * @param deleted the deleted nodes
+ * @param moved nodes that were moved (e.g. deleted, but also inserted elsewhere)
+ * @param layout the parent layout of the deleted nodes
+ */
+ public DeletionHandler(@NonNull List<INode> deleted, @NonNull List<INode> moved,
+ @NonNull INode layout) {
+ mDeleted = deleted;
+ mMoved = moved;
+ mLayout = layout;
+
+ mChildren = mLayout.getChildren();
+ mNodeMap = Maps.newHashMapWithExpectedSize(mChildren.length);
+ for (INode child : mChildren) {
+ String id = child.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id != null) {
+ mNodeMap.put(stripIdPrefix(id), child);
+ }
+ }
+
+ mDeletedIds = Sets.newHashSetWithExpectedSize(mDeleted.size());
+ for (INode node : mDeleted) {
+ String id = node.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id != null) {
+ mDeletedIds.add(stripIdPrefix(id));
+ }
+ }
+
+ // Any widgets that remain (e.g. typically because they were moved) should
+ // keep their incoming dependencies
+ for (INode node : mMoved) {
+ String id = node.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id != null) {
+ mDeletedIds.remove(stripIdPrefix(id));
+ }
+ }
+ }
+
+ @Nullable
+ private static String getId(@NonNull IAttribute attribute) {
+ if (attribute.getName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
+ && ANDROID_URI.equals(attribute.getUri())
+ && !attribute.getName().startsWith(ATTR_LAYOUT_MARGIN)) {
+ String id = attribute.getValue();
+ // It might not be an id reference, so check manually rather than just
+ // calling stripIdPrefix():
+ if (id.startsWith(NEW_ID_PREFIX)) {
+ return id.substring(NEW_ID_PREFIX.length());
+ } else if (id.startsWith(ID_PREFIX)) {
+ return id.substring(ID_PREFIX.length());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Updates the constraints in the layout to handle deletion of a set of
+ * nodes. This ensures that any constraints pointing to one of the deleted
+ * nodes are changed properly to point to a non-deleted node with similar
+ * constraints.
+ */
+ public void updateConstraints() {
+ if (mChildren.length == mDeleted.size()) {
+ // Deleting everything: Nothing to be done
+ return;
+ }
+
+ // Now remove incoming edges to any views that were deleted. If possible,
+ // don't just delete them but replace them with a transitive constraint, e.g.
+ // if we have "A <= B <= C" and "B" is removed, then we end up with "A <= C",
+
+ for (INode child : mChildren) {
+ if (mDeleted.contains(child)) {
+ continue;
+ }
+
+ for (IAttribute attribute : child.getLiveAttributes()) {
+ String id = getId(attribute);
+ if (id != null) {
+ if (mDeletedIds.contains(id)) {
+ // Unset this reference to a deleted widget. It might be
+ // replaced if the pointed to node points to some other node
+ // on the same side, but it may use a different constraint name,
+ // or have none at all (e.g. parent).
+ String name = attribute.getName();
+ child.setAttribute(ANDROID_URI, name, null);
+
+ INode deleted = mNodeMap.get(id);
+ if (deleted != null) {
+ ConstraintType type = ConstraintType.fromAttribute(name);
+ if (type != null) {
+ transfer(deleted, child, type, 0);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void transfer(INode deleted, INode target, ConstraintType targetType, int depth) {
+ if (depth == 20) {
+ // Prevent really deep flow or unbounded recursion in case there is a bug in
+ // the cycle detection code
+ return;
+ }
+
+ assert mDeleted.contains(deleted);
+
+ for (IAttribute attribute : deleted.getLiveAttributes()) {
+ String name = attribute.getName();
+ ConstraintType type = ConstraintType.fromAttribute(name);
+ if (type == null) {
+ continue;
+ }
+
+ ConstraintType transfer = getCompatibleConstraint(type, targetType);
+ if (transfer != null) {
+ String id = getId(attribute);
+ if (id != null) {
+ if (mDeletedIds.contains(id)) {
+ INode nextDeleted = mNodeMap.get(id);
+ if (nextDeleted != null) {
+ // Points to another deleted node: recurse
+ transfer(nextDeleted, target, targetType, depth + 1);
+ }
+ } else {
+ // Found an undeleted node destination: point to it directly.
+ // Note that we're using the
+ target.setAttribute(ANDROID_URI, transfer.name, attribute.getValue());
+ }
+ } else {
+ // Pointing to parent or center etc (non-id ref): replicate this on the target
+ target.setAttribute(ANDROID_URI, name, attribute.getValue());
+ }
+ }
+ }
+ }
+
+ /**
+ * Determines if two constraints are in the same direction and if so returns
+ * the constraint in the same direction. Rather than returning boolean true
+ * or false, this returns the constraint which is sometimes modified. For
+ * example, if you have a node which points left to a node which is centered
+ * in parent, then the constraint is turned into center horizontal.
+ */
+ @Nullable
+ private static ConstraintType getCompatibleConstraint(
+ @NonNull ConstraintType first, @NonNull ConstraintType second) {
+ if (first == second) {
+ return first;
+ }
+
+ switch (second) {
+ case ALIGN_LEFT:
+ case LAYOUT_RIGHT_OF:
+ switch (first) {
+ case LAYOUT_CENTER_HORIZONTAL:
+ case LAYOUT_LEFT_OF:
+ case ALIGN_LEFT:
+ return first;
+ case LAYOUT_CENTER_IN_PARENT:
+ return LAYOUT_CENTER_HORIZONTAL;
+ }
+ return null;
+
+ case ALIGN_RIGHT:
+ case LAYOUT_LEFT_OF:
+ switch (first) {
+ case LAYOUT_CENTER_HORIZONTAL:
+ case ALIGN_RIGHT:
+ case LAYOUT_LEFT_OF:
+ return first;
+ case LAYOUT_CENTER_IN_PARENT:
+ return LAYOUT_CENTER_HORIZONTAL;
+ }
+ return null;
+
+ case ALIGN_TOP:
+ case LAYOUT_BELOW:
+ case ALIGN_BASELINE:
+ switch (first) {
+ case LAYOUT_CENTER_VERTICAL:
+ case ALIGN_TOP:
+ case LAYOUT_BELOW:
+ case ALIGN_BASELINE:
+ return first;
+ case LAYOUT_CENTER_IN_PARENT:
+ return LAYOUT_CENTER_VERTICAL;
+ }
+ return null;
+ case ALIGN_BOTTOM:
+ case LAYOUT_ABOVE:
+ switch (first) {
+ case LAYOUT_CENTER_VERTICAL:
+ case ALIGN_BOTTOM:
+ case LAYOUT_ABOVE:
+ case ALIGN_BASELINE:
+ return first;
+ case LAYOUT_CENTER_IN_PARENT:
+ return LAYOUT_CENTER_VERTICAL;
+ }
+ return null;
+ }
+
+ return null;
+ }
+}
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;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java
new file mode 100644
index 000000000..db08b1857
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java
@@ -0,0 +1,839 @@
+/*
+ * 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.ide.common.api.MarginType.NO_MARGIN;
+import static com.android.ide.common.api.MarginType.WITHOUT_MARGIN;
+import static com.android.ide.common.api.MarginType.WITH_MARGIN;
+import static com.android.ide.common.api.SegmentType.BASELINE;
+import static com.android.ide.common.api.SegmentType.BOTTOM;
+import static com.android.ide.common.api.SegmentType.CENTER_HORIZONTAL;
+import static com.android.ide.common.api.SegmentType.CENTER_VERTICAL;
+import static com.android.ide.common.api.SegmentType.LEFT;
+import static com.android.ide.common.api.SegmentType.RIGHT;
+import static com.android.ide.common.api.SegmentType.TOP;
+import static com.android.ide.common.layout.BaseLayoutRule.getMaxMatchDistance;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_IN_PARENT;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
+import static com.android.SdkConstants.VALUE_N_DP;
+import static com.android.SdkConstants.VALUE_TRUE;
+import static com.android.ide.common.layout.relative.ConstraintType.ALIGN_BASELINE;
+
+import static java.lang.Math.abs;
+
+import com.android.SdkConstants;
+import static com.android.SdkConstants.ANDROID_URI;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IClientRulesEngine;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.Segment;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.common.layout.BaseLayoutRule;
+import com.android.ide.common.layout.relative.DependencyGraph.Constraint;
+import com.android.ide.common.layout.relative.DependencyGraph.ViewData;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The {@link GuidelineHandler} class keeps track of state related to a guideline operation
+ * like move and resize, and performs various constraint computations.
+ */
+public class GuidelineHandler {
+ /**
+ * A dependency graph for the relative layout recording constraint relationships
+ */
+ protected DependencyGraph mDependencyGraph;
+
+ /** The RelativeLayout we are moving/resizing within */
+ public INode layout;
+
+ /** The set of nodes being dragged (may be null) */
+ protected Collection<INode> mDraggedNodes;
+
+ /** The bounds of the primary child node being dragged */
+ protected Rect mBounds;
+
+ /** Whether the left edge is being moved/resized */
+ protected boolean mMoveLeft;
+
+ /** Whether the right edge is being moved/resized */
+ protected boolean mMoveRight;
+
+ /** Whether the top edge is being moved/resized */
+ protected boolean mMoveTop;
+
+ /** Whether the bottom edge is being moved/resized */
+ protected boolean mMoveBottom;
+
+ /**
+ * Whether the drop/move/resize position should be snapped (which can be turned off
+ * with a modifier key during the operation)
+ */
+ protected boolean mSnap = true;
+
+ /**
+ * The set of nodes which depend on the currently selected nodes, including
+ * transitively, through horizontal constraints (a "horizontal constraint"
+ * is a constraint between two horizontal edges)
+ */
+ protected Set<INode> mHorizontalDeps;
+
+ /**
+ * The set of nodes which depend on the currently selected nodes, including
+ * transitively, through vertical constraints (a "vertical constraint"
+ * is a constraint between two vertical edges)
+ */
+ protected Set<INode> mVerticalDeps;
+
+ /** The current list of constraints which result in a horizontal cycle (if applicable) */
+ protected List<Constraint> mHorizontalCycle;
+
+ /** The current list of constraints which result in a vertical cycle (if applicable) */
+ protected List<Constraint> mVerticalCycle;
+
+ /**
+ * All horizontal segments in the relative layout - top and bottom edges, baseline
+ * edges, and top and bottom edges offset by the applicable margins in each direction
+ */
+ protected List<Segment> mHorizontalEdges;
+
+ /**
+ * All vertical segments in the relative layout - left and right edges, and left and
+ * right edges offset by the applicable margins in each direction
+ */
+ protected List<Segment> mVerticalEdges;
+
+ /**
+ * All center vertical segments in the relative layout. These are kept separate since
+ * they only match other center edges.
+ */
+ protected List<Segment> mCenterVertEdges;
+
+ /**
+ * All center horizontal segments in the relative layout. These are kept separate
+ * since they only match other center edges.
+ */
+ protected List<Segment> mCenterHorizEdges;
+
+ /**
+ * Suggestions for horizontal matches. There could be more than one, but all matches
+ * will be equidistant from the current position (as well as in the same direction,
+ * which means that you can't have one match 5 pixels to the left and one match 5
+ * pixels to the right since it would be impossible to snap to fit with both; you can
+ * however have multiple matches all 5 pixels to the left.)
+ * <p
+ * The best vertical match will be found in {@link #mCurrentTopMatch} or
+ * {@link #mCurrentBottomMatch}.
+ */
+ protected List<Match> mHorizontalSuggestions;
+
+ /**
+ * Suggestions for vertical matches.
+ * <p
+ * The best vertical match will be found in {@link #mCurrentLeftMatch} or
+ * {@link #mCurrentRightMatch}.
+ */
+ protected List<Match> mVerticalSuggestions;
+
+ /**
+ * The current match on the left edge, or null if no match or if the left edge is not
+ * being moved or resized.
+ */
+ protected Match mCurrentLeftMatch;
+
+ /**
+ * The current match on the top edge, or null if no match or if the top edge is not
+ * being moved or resized.
+ */
+ protected Match mCurrentTopMatch;
+
+ /**
+ * The current match on the right edge, or null if no match or if the right edge is
+ * not being moved or resized.
+ */
+ protected Match mCurrentRightMatch;
+
+ /**
+ * The current match on the bottom edge, or null if no match or if the bottom edge is
+ * not being moved or resized.
+ */
+ protected Match mCurrentBottomMatch;
+
+ /**
+ * The amount of margin to add to the top edge, or 0
+ */
+ protected int mTopMargin;
+
+ /**
+ * The amount of margin to add to the bottom edge, or 0
+ */
+ protected int mBottomMargin;
+
+ /**
+ * The amount of margin to add to the left edge, or 0
+ */
+ protected int mLeftMargin;
+
+ /**
+ * The amount of margin to add to the right edge, or 0
+ */
+ protected int mRightMargin;
+
+ /**
+ * The associated rules engine
+ */
+ protected IClientRulesEngine mRulesEngine;
+
+ /**
+ * Construct a new {@link GuidelineHandler} for the given relative layout.
+ *
+ * @param layout the RelativeLayout to handle
+ */
+ GuidelineHandler(INode layout, IClientRulesEngine rulesEngine) {
+ this.layout = layout;
+ mRulesEngine = rulesEngine;
+
+ mHorizontalEdges = new ArrayList<Segment>();
+ mVerticalEdges = new ArrayList<Segment>();
+ mCenterVertEdges = new ArrayList<Segment>();
+ mCenterHorizEdges = new ArrayList<Segment>();
+ mDependencyGraph = new DependencyGraph(layout);
+ }
+
+ /**
+ * Returns true if the handler has any suggestions to offer
+ *
+ * @return true if the handler has any suggestions to offer
+ */
+ public boolean haveSuggestions() {
+ return mCurrentLeftMatch != null || mCurrentTopMatch != null
+ || mCurrentRightMatch != null || mCurrentBottomMatch != null;
+ }
+
+ /**
+ * Returns the closest match.
+ *
+ * @return the closest match, or null if nothing matched
+ */
+ protected Match pickBestMatch(List<Match> matches) {
+ int alternatives = matches.size();
+ if (alternatives == 0) {
+ return null;
+ } else if (alternatives == 1) {
+ Match match = matches.get(0);
+ return match;
+ } else {
+ assert alternatives > 1;
+ Collections.sort(matches, new MatchComparator());
+ return matches.get(0);
+ }
+ }
+
+ private boolean checkCycle(DropFeedback feedback, Match match, boolean vertical) {
+ if (match != null && match.cycle) {
+ for (INode node : mDraggedNodes) {
+ INode from = match.edge.node;
+ assert match.with.node == null || match.with.node == node;
+ INode to = node;
+ List<Constraint> path = mDependencyGraph.getPathTo(from, to, vertical);
+ if (path != null) {
+ if (vertical) {
+ mVerticalCycle = path;
+ } else {
+ mHorizontalCycle = path;
+ }
+ String desc = Constraint.describePath(path,
+ match.type.name, match.edge.id);
+
+ feedback.errorMessage = "Constraint creates a cycle: " + desc;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks for any cycles in the dependencies
+ *
+ * @param feedback the drop feedback state
+ */
+ public void checkCycles(DropFeedback feedback) {
+ // Deliberate short circuit evaluation -- only list the first cycle
+ feedback.errorMessage = null;
+ mHorizontalCycle = null;
+ mVerticalCycle = null;
+
+ if (checkCycle(feedback, mCurrentTopMatch, true /* vertical */)
+ || checkCycle(feedback, mCurrentBottomMatch, true)) {
+ }
+
+ if (checkCycle(feedback, mCurrentLeftMatch, false)
+ || checkCycle(feedback, mCurrentRightMatch, false)) {
+ }
+ }
+
+ /** Records the matchable outside edges for the given node to the potential match list */
+ protected void addBounds(INode node, String id,
+ boolean addHorizontal, boolean addVertical) {
+ Rect b = node.getBounds();
+ Margins margins = node.getMargins();
+ if (addHorizontal) {
+ if (margins.top != 0) {
+ mHorizontalEdges.add(new Segment(b.y, b.x, b.x2(), node, id, TOP, WITHOUT_MARGIN));
+ mHorizontalEdges.add(new Segment(b.y - margins.top, b.x, b.x2(), node, id,
+ TOP, WITH_MARGIN));
+ } else {
+ mHorizontalEdges.add(new Segment(b.y, b.x, b.x2(), node, id, TOP, NO_MARGIN));
+ }
+ if (margins.bottom != 0) {
+ mHorizontalEdges.add(new Segment(b.y2(), b.x, b.x2(), node, id, BOTTOM,
+ WITHOUT_MARGIN));
+ mHorizontalEdges.add(new Segment(b.y2() + margins.bottom, b.x, b.x2(), node,
+ id, BOTTOM, WITH_MARGIN));
+ } else {
+ mHorizontalEdges.add(new Segment(b.y2(), b.x, b.x2(), node, id,
+ BOTTOM, NO_MARGIN));
+ }
+ }
+ if (addVertical) {
+ if (margins.left != 0) {
+ mVerticalEdges.add(new Segment(b.x, b.y, b.y2(), node, id, LEFT, WITHOUT_MARGIN));
+ mVerticalEdges.add(new Segment(b.x - margins.left, b.y, b.y2(), node, id, LEFT,
+ WITH_MARGIN));
+ } else {
+ mVerticalEdges.add(new Segment(b.x, b.y, b.y2(), node, id, LEFT, NO_MARGIN));
+ }
+
+ if (margins.right != 0) {
+ mVerticalEdges.add(new Segment(b.x2(), b.y, b.y2(), node, id,
+ RIGHT, WITHOUT_MARGIN));
+ mVerticalEdges.add(new Segment(b.x2() + margins.right, b.y, b.y2(), node, id,
+ RIGHT, WITH_MARGIN));
+ } else {
+ mVerticalEdges.add(new Segment(b.x2(), b.y, b.y2(), node, id,
+ RIGHT, NO_MARGIN));
+ }
+ }
+ }
+
+ /** Records the center edges for the given node to the potential match list */
+ protected void addCenter(INode node, String id,
+ boolean addHorizontal, boolean addVertical) {
+ Rect b = node.getBounds();
+
+ if (addHorizontal) {
+ mCenterHorizEdges.add(new Segment(b.centerY(), b.x, b.x2(),
+ node, id, CENTER_HORIZONTAL, NO_MARGIN));
+ }
+ if (addVertical) {
+ mCenterVertEdges.add(new Segment(b.centerX(), b.y, b.y2(),
+ node, id, CENTER_VERTICAL, NO_MARGIN));
+ }
+ }
+
+ /** Records the baseline edge for the given node to the potential match list */
+ protected int addBaseLine(INode node, String id) {
+ int baselineY = node.getBaseline();
+ if (baselineY != -1) {
+ Rect b = node.getBounds();
+ mHorizontalEdges.add(new Segment(b.y + baselineY, b.x, b.x2(), node, id, BASELINE,
+ NO_MARGIN));
+ }
+
+ return baselineY;
+ }
+
+ protected void snapVertical(Segment vEdge, int x, Rect newBounds) {
+ newBounds.x = x;
+ }
+
+ protected void snapHorizontal(Segment hEdge, int y, Rect newBounds) {
+ newBounds.y = y;
+ }
+
+ /**
+ * Returns whether two edge types are compatible. For example, we only match the
+ * center of one object with the center of another.
+ *
+ * @param edge the first edge type to compare
+ * @param dragged the second edge type to compare the first one with
+ * @param delta the delta between the two edge locations
+ * @return true if the two edge types can be compatibly matched
+ */
+ protected boolean isEdgeTypeCompatible(SegmentType edge, SegmentType dragged, int delta) {
+
+ if (Math.abs(delta) > BaseLayoutRule.getMaxMatchDistance()) {
+ if (dragged == LEFT || dragged == TOP) {
+ if (delta > 0) {
+ return false;
+ }
+ } else {
+ if (delta < 0) {
+ return false;
+ }
+ }
+ }
+
+ switch (edge) {
+ case BOTTOM:
+ case TOP:
+ return dragged == TOP || dragged == BOTTOM;
+ case LEFT:
+ case RIGHT:
+ return dragged == LEFT || dragged == RIGHT;
+
+ // Center horizontal, center vertical and Baseline only matches the same
+ // type, and only within the matching distance -- no margins!
+ case BASELINE:
+ case CENTER_HORIZONTAL:
+ case CENTER_VERTICAL:
+ return dragged == edge && Math.abs(delta) < getMaxMatchDistance();
+ default: assert false : edge;
+ }
+ return false;
+ }
+
+ /**
+ * Finds the closest matching segments among the given list of edges for the given
+ * dragged edge, and returns these as a list of matches
+ */
+ protected List<Match> findClosest(Segment draggedEdge, List<Segment> edges) {
+ List<Match> closest = new ArrayList<Match>();
+ addClosest(draggedEdge, edges, closest);
+ return closest;
+ }
+
+ protected void addClosest(Segment draggedEdge, List<Segment> edges,
+ List<Match> closest) {
+ int at = draggedEdge.at;
+ int closestDelta = closest.size() > 0 ? closest.get(0).delta : Integer.MAX_VALUE;
+ int closestDistance = abs(closestDelta);
+ for (Segment edge : edges) {
+ assert draggedEdge.edgeType.isHorizontal() == edge.edgeType.isHorizontal();
+
+ int delta = edge.at - at;
+ int distance = abs(delta);
+ if (distance > closestDistance) {
+ continue;
+ }
+
+ if (!isEdgeTypeCompatible(edge.edgeType, draggedEdge.edgeType, delta)) {
+ continue;
+ }
+
+ boolean withParent = edge.node == layout;
+ ConstraintType type = ConstraintType.forMatch(withParent,
+ draggedEdge.edgeType, edge.edgeType);
+ if (type == null) {
+ continue;
+ }
+
+ // Ensure that the edge match is compatible; for example, a "below"
+ // constraint can only apply to the margin bounds and a "bottom"
+ // constraint can only apply to the non-margin bounds.
+ if (type.relativeToMargin && edge.marginType == WITHOUT_MARGIN) {
+ continue;
+ } else if (!type.relativeToMargin && edge.marginType == WITH_MARGIN) {
+ continue;
+ }
+
+ Match match = new Match(this, edge, draggedEdge, type, delta);
+
+ if (distance < closestDistance) {
+ closest.clear();
+ closestDistance = distance;
+ closestDelta = delta;
+ } else if (delta * closestDelta < 0) {
+ // They have different signs, e.g. the matches are equal but
+ // on opposite sides; can't accept them both
+ continue;
+ }
+ closest.add(match);
+ }
+ }
+
+ protected void clearSuggestions() {
+ mHorizontalSuggestions = mVerticalSuggestions = null;
+ mCurrentLeftMatch = mCurrentRightMatch = null;
+ mCurrentTopMatch = mCurrentBottomMatch = null;
+ }
+
+ /**
+ * Given a node, apply the suggestions by expressing them as relative layout param
+ * values
+ *
+ * @param n the node to apply constraints to
+ */
+ public void applyConstraints(INode n) {
+ // Process each edge separately
+ String centerBoth = n.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT);
+ if (centerBoth != null && centerBoth.equals(VALUE_TRUE)) {
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, null);
+
+ // If you had a center-in-both-directions attribute, and you're
+ // only resizing in one dimension, then leave the other dimension
+ // centered, e.g. if you have centerInParent and apply alignLeft,
+ // then you should end up with alignLeft and centerVertically
+ if (mCurrentTopMatch == null && mCurrentBottomMatch == null) {
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, VALUE_TRUE);
+ }
+ if (mCurrentLeftMatch == null && mCurrentRightMatch == null) {
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, VALUE_TRUE);
+ }
+ }
+
+ if (mMoveTop) {
+ // Remove top attachments
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_TOP, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_TOP, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_BELOW, null);
+
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null);
+
+ }
+
+ if (mMoveBottom) {
+ // Remove bottom attachments
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BOTTOM, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ABOVE, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null);
+ }
+
+ if (mMoveLeft) {
+ // Remove left attachments
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_LEFT, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_LEFT, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
+ }
+
+ if (mMoveRight) {
+ // Remove right attachments
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_RIGHT, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_RIGHT, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
+ }
+
+ if (mMoveTop && mCurrentTopMatch != null) {
+ applyConstraint(n, mCurrentTopMatch.getConstraint(true /* generateId */));
+ if (mCurrentTopMatch.type == ALIGN_BASELINE) {
+ // HACK! WORKAROUND! Baseline doesn't provide a new bottom edge for attachments
+ String c = mCurrentTopMatch.getConstraint(true);
+ c = c.replace(ATTR_LAYOUT_ALIGN_BASELINE, ATTR_LAYOUT_ALIGN_BOTTOM);
+ applyConstraint(n, c);
+ }
+ }
+
+ if (mMoveBottom && mCurrentBottomMatch != null) {
+ applyConstraint(n, mCurrentBottomMatch.getConstraint(true));
+ }
+
+ if (mMoveLeft && mCurrentLeftMatch != null) {
+ applyConstraint(n, mCurrentLeftMatch.getConstraint(true));
+ }
+
+ if (mMoveRight && mCurrentRightMatch != null) {
+ applyConstraint(n, mCurrentRightMatch.getConstraint(true));
+ }
+
+ if (mMoveLeft) {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_LEFT, mLeftMargin);
+ }
+ if (mMoveRight) {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_RIGHT, mRightMargin);
+ }
+ if (mMoveTop) {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_TOP, mTopMargin);
+ }
+ if (mMoveBottom) {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_BOTTOM, mBottomMargin);
+ }
+ }
+
+ private void applyConstraint(INode n, String constraint) {
+ assert constraint.contains("=") : constraint;
+ String name = constraint.substring(0, constraint.indexOf('='));
+ String value = constraint.substring(constraint.indexOf('=') + 1);
+ n.setAttribute(ANDROID_URI, name, value);
+ }
+
+ private void applyMargin(INode n, String marginAttribute, int margin) {
+ if (margin > 0) {
+ int dp = mRulesEngine.pxToDp(margin);
+ n.setAttribute(ANDROID_URI, marginAttribute, String.format(VALUE_N_DP, dp));
+ } else if (n.getStringAttr(ANDROID_URI, marginAttribute) != null) {
+ // Clear out existing margin
+ n.setAttribute(ANDROID_URI, marginAttribute, null);
+ }
+ }
+
+ private void removeRelativeParams(INode node) {
+ for (ConstraintType type : ConstraintType.values()) {
+ node.setAttribute(ANDROID_URI, type.name, null);
+ }
+ node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_LEFT, null);
+ node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_RIGHT, null);
+ node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_TOP, null);
+ node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_BOTTOM, null);
+ }
+
+ /**
+ * Attach the new child to the previous node
+ * @param previous the previous child
+ * @param node the new child to attach it to
+ */
+ public void attachPrevious(INode previous, INode node) {
+ removeRelativeParams(node);
+
+ String id = previous.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id == null) {
+ return;
+ }
+
+ if (mCurrentTopMatch != null || mCurrentBottomMatch != null) {
+ // Attaching the top: arrange below, and for bottom arrange above
+ node.setAttribute(ANDROID_URI,
+ mCurrentTopMatch != null ? ATTR_LAYOUT_BELOW : ATTR_LAYOUT_ABOVE, id);
+ // Apply same left/right constraints as the parent
+ if (mCurrentLeftMatch != null) {
+ applyConstraint(node, mCurrentLeftMatch.getConstraint(true));
+ applyMargin(node, ATTR_LAYOUT_MARGIN_LEFT, mLeftMargin);
+ } else if (mCurrentRightMatch != null) {
+ applyConstraint(node, mCurrentRightMatch.getConstraint(true));
+ applyMargin(node, ATTR_LAYOUT_MARGIN_RIGHT, mRightMargin);
+ }
+ } else if (mCurrentLeftMatch != null || mCurrentRightMatch != null) {
+ node.setAttribute(ANDROID_URI,
+ mCurrentLeftMatch != null ? ATTR_LAYOUT_TO_RIGHT_OF : ATTR_LAYOUT_TO_LEFT_OF,
+ id);
+ // Apply same top/bottom constraints as the parent
+ if (mCurrentTopMatch != null) {
+ applyConstraint(node, mCurrentTopMatch.getConstraint(true));
+ applyMargin(node, ATTR_LAYOUT_MARGIN_TOP, mTopMargin);
+ } else if (mCurrentBottomMatch != null) {
+ applyConstraint(node, mCurrentBottomMatch.getConstraint(true));
+ applyMargin(node, ATTR_LAYOUT_MARGIN_BOTTOM, mBottomMargin);
+ }
+ } else {
+ return;
+ }
+ }
+
+ /** Breaks any cycles detected by the handler */
+ public void removeCycles() {
+ if (mHorizontalCycle != null) {
+ removeCycles(mHorizontalDeps);
+ }
+ if (mVerticalCycle != null) {
+ removeCycles(mVerticalDeps);
+ }
+ }
+
+ private void removeCycles(Set<INode> deps) {
+ for (INode node : mDraggedNodes) {
+ ViewData view = mDependencyGraph.getView(node);
+ if (view != null) {
+ for (Constraint constraint : view.dependedOnBy) {
+ // For now, remove ALL constraints pointing to this node in this orientation.
+ // Later refine this to be smarter. (We can't JUST remove the constraints
+ // identified in the cycle since there could be multiple.)
+ constraint.from.node.setAttribute(ANDROID_URI, constraint.type.name, null);
+ }
+ }
+ }
+ }
+
+ /**
+ * Comparator used to sort matches such that the first match is the most desirable
+ * match (where we prefer attaching to parent bounds, we avoid matches that lead to a
+ * cycle, we prefer constraints on closer widgets rather than ones further away, and
+ * so on.)
+ * <p>
+ * There are a number of sorting criteria. One of them is the distance between the
+ * matched edges. We may end up with multiple matches that are the same distance. In
+ * that case we look at the orientation; on the left side, prefer left-oriented
+ * attachments, and on the right-side prefer right-oriented attachments. For example,
+ * consider the following scenario:
+ *
+ * <pre>
+ * +--------------------+-------------------------+
+ * | Attached on left | |
+ * +--------------------+ |
+ * | |
+ * | +-----+ |
+ * | | A | |
+ * | +-----+ |
+ * | |
+ * | +-------------------------+
+ * | | Attached on right |
+ * +--------------------+-------------------------+
+ * </pre>
+ *
+ * Here, dragging the left edge should attach to the top left attached view, whereas
+ * in the following layout dragging the right edge would attach to the bottom view:
+ *
+ * <pre>
+ * +--------------------------+-------------------+
+ * | Attached on left | |
+ * +--------------------------+ |
+ * | |
+ * | +-----+ |
+ * | | A | |
+ * | +-----+ |
+ * | |
+ * | +-------------------+
+ * | | Attached on right |
+ * +--------------------------+-------------------+
+ *
+ * </pre>
+ *
+ * </ul>
+ */
+ private final class MatchComparator implements Comparator<Match> {
+ @Override
+ public int compare(Match m1, Match m2) {
+ // Always prefer matching parent bounds
+ int parent1 = m1.edge.node == layout ? -1 : 1;
+ int parent2 = m2.edge.node == layout ? -1 : 1;
+ // unless it's a center bound -- those should always get lowest priority since
+ // they overlap with other usually more interesting edges near the center of
+ // the layout.
+ if (m1.edge.edgeType == CENTER_HORIZONTAL
+ || m1.edge.edgeType == CENTER_VERTICAL) {
+ parent1 = 2;
+ }
+ if (m2.edge.edgeType == CENTER_HORIZONTAL
+ || m2.edge.edgeType == CENTER_VERTICAL) {
+ parent2 = 2;
+ }
+ if (parent1 != parent2) {
+ return parent1 - parent2;
+ }
+
+ // Avoid matching edges that would lead to a cycle
+ if (m1.edge.edgeType.isHorizontal()) {
+ int cycle1 = mHorizontalDeps.contains(m1.edge.node) ? 1 : -1;
+ int cycle2 = mHorizontalDeps.contains(m2.edge.node) ? 1 : -1;
+ if (cycle1 != cycle2) {
+ return cycle1 - cycle2;
+ }
+ } else {
+ int cycle1 = mVerticalDeps.contains(m1.edge.node) ? 1 : -1;
+ int cycle2 = mVerticalDeps.contains(m2.edge.node) ? 1 : -1;
+ if (cycle1 != cycle2) {
+ return cycle1 - cycle2;
+ }
+ }
+
+ // TODO: Sort by minimum depth -- do we have the depth anywhere?
+
+ // Prefer nodes that are closer
+ int distance1, distance2;
+ if (m1.edge.to <= m1.with.from) {
+ distance1 = m1.with.from - m1.edge.to;
+ } else if (m1.edge.from >= m1.with.to) {
+ distance1 = m1.edge.from - m1.with.to;
+ } else {
+ // Some kind of overlap - not sure how to prioritize these yet...
+ distance1 = 0;
+ }
+ if (m2.edge.to <= m2.with.from) {
+ distance2 = m2.with.from - m2.edge.to;
+ } else if (m2.edge.from >= m2.with.to) {
+ distance2 = m2.edge.from - m2.with.to;
+ } else {
+ // Some kind of overlap - not sure how to prioritize these yet...
+ distance2 = 0;
+ }
+
+ if (distance1 != distance2) {
+ return distance1 - distance2;
+ }
+
+ // Prefer matching on baseline
+ int baseline1 = (m1.edge.edgeType == BASELINE) ? -1 : 1;
+ int baseline2 = (m2.edge.edgeType == BASELINE) ? -1 : 1;
+ if (baseline1 != baseline2) {
+ return baseline1 - baseline2;
+ }
+
+ // Prefer matching top/left edges before matching bottom/right edges
+ int orientation1 = (m1.with.edgeType == LEFT ||
+ m1.with.edgeType == TOP) ? -1 : 1;
+ int orientation2 = (m2.with.edgeType == LEFT ||
+ m2.with.edgeType == TOP) ? -1 : 1;
+ if (orientation1 != orientation2) {
+ return orientation1 - orientation2;
+ }
+
+ // Prefer opposite-matching over same-matching.
+ // In other words, if we have the choice of matching
+ // our left edge with another element's left edge,
+ // or matching our left edge with another element's right
+ // edge, prefer the right edge since that
+ // The two matches have identical distance; try to sort by
+ // orientation
+ int edgeType1 = (m1.edge.edgeType != m1.with.edgeType) ? -1 : 1;
+ int edgeType2 = (m2.edge.edgeType != m2.with.edgeType) ? -1 : 1;
+ if (edgeType1 != edgeType2) {
+ return edgeType1 - edgeType2;
+ }
+
+ return 0;
+ }
+ }
+
+ /**
+ * Returns the {@link IClientRulesEngine} IDE callback
+ *
+ * @return the {@link IClientRulesEngine} IDE callback, never null
+ */
+ public IClientRulesEngine getRulesEngine() {
+ return mRulesEngine;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelinePainter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelinePainter.java
new file mode 100644
index 000000000..2fe74768f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelinePainter.java
@@ -0,0 +1,208 @@
+/*
+ * 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_LAYOUT_MARGIN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IFeedbackPainter;
+import com.android.ide.common.api.IGraphics;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Point;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.common.layout.relative.DependencyGraph.Constraint;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The {@link GuidelinePainter} is responsible for painting guidelines during an operation
+ * which uses a {@link GuidelineHandler} such as a resize operation.
+ */
+public final class GuidelinePainter implements IFeedbackPainter {
+ // ---- Implements IFeedbackPainter ----
+ @Override
+ public void paint(@NonNull IGraphics gc, @NonNull INode node, @NonNull DropFeedback feedback) {
+ GuidelineHandler state = (GuidelineHandler) feedback.userData;
+
+ for (INode dragged : state.mDraggedNodes) {
+ gc.useStyle(DrawingStyle.DRAGGED);
+ Rect bounds = dragged.getBounds();
+ if (bounds.isValid()) {
+ gc.fillRect(bounds);
+ }
+ }
+
+ Set<INode> horizontalDeps = state.mHorizontalDeps;
+ Set<INode> verticalDeps = state.mVerticalDeps;
+ Set<INode> deps = new HashSet<INode>(horizontalDeps.size() + verticalDeps.size());
+ deps.addAll(horizontalDeps);
+ deps.addAll(verticalDeps);
+ if (deps.size() > 0) {
+ gc.useStyle(DrawingStyle.DEPENDENCY);
+ for (INode n : deps) {
+ // Don't highlight the selected nodes themselves
+ if (state.mDraggedNodes.contains(n)) {
+ continue;
+ }
+ Rect bounds = n.getBounds();
+ gc.fillRect(bounds);
+ }
+ }
+
+ if (state.mBounds != null) {
+ if (state instanceof MoveHandler) {
+ gc.useStyle(DrawingStyle.DROP_PREVIEW);
+ } else {
+ // Resizing
+ if (state.haveSuggestions()) {
+ gc.useStyle(DrawingStyle.RESIZE_PREVIEW);
+ } else {
+ gc.useStyle(DrawingStyle.RESIZE_FAIL);
+ }
+ }
+ gc.drawRect(state.mBounds);
+
+ // Draw baseline preview too
+ if (feedback.dragBaseline != -1) {
+ int y = state.mBounds.y + feedback.dragBaseline;
+ gc.drawLine(state.mBounds.x, y, state.mBounds.x2(), y);
+ }
+ }
+
+ List<String> strings = new ArrayList<String>();
+
+ showMatch(gc, state.mCurrentLeftMatch, state, strings,
+ state.mLeftMargin, ATTR_LAYOUT_MARGIN_LEFT);
+ showMatch(gc, state.mCurrentRightMatch, state, strings,
+ state.mRightMargin, ATTR_LAYOUT_MARGIN_RIGHT);
+ showMatch(gc, state.mCurrentTopMatch, state, strings,
+ state.mTopMargin, ATTR_LAYOUT_MARGIN_TOP);
+ showMatch(gc, state.mCurrentBottomMatch, state, strings,
+ state.mBottomMargin, ATTR_LAYOUT_MARGIN_BOTTOM);
+
+ if (strings.size() > 0) {
+ // Update the drag tooltip
+ StringBuilder sb = new StringBuilder(200);
+ for (String s : strings) {
+ if (sb.length() > 0) {
+ sb.append('\n');
+ }
+ sb.append(s);
+ }
+ feedback.tooltip = sb.toString();
+
+ // Set the tooltip orientation to ensure that it does not interfere with
+ // the constraint arrows
+ if (state.mCurrentLeftMatch != null) {
+ feedback.tooltipX = SegmentType.RIGHT;
+ } else if (state.mCurrentRightMatch != null) {
+ feedback.tooltipX = SegmentType.LEFT;
+ }
+ if (state.mCurrentTopMatch != null) {
+ feedback.tooltipY = SegmentType.BOTTOM;
+ } else if (state.mCurrentBottomMatch != null) {
+ feedback.tooltipY = SegmentType.TOP;
+ }
+ } else {
+ feedback.tooltip = null;
+ }
+
+ if (state.mHorizontalCycle != null) {
+ paintCycle(gc, state, state.mHorizontalCycle);
+ }
+ if (state.mVerticalCycle != null) {
+ paintCycle(gc, state, state.mVerticalCycle);
+ }
+ }
+
+ /** Paints a particular match constraint */
+ private void showMatch(IGraphics gc, Match m, GuidelineHandler state, List<String> strings,
+ int margin, String marginAttribute) {
+ if (m == null) {
+ return;
+ }
+ ConstraintPainter.paintConstraint(gc, state.mBounds, m);
+
+ // Display the constraint. Remove the @id/ and @+id/ prefixes to make the text
+ // shorter and easier to read. This doesn't use stripPrefix() because the id is
+ // usually not a prefix of the value (for example, 'layout_alignBottom=@+id/foo').
+ String constraint = m.getConstraint(false /* generateId */);
+ String description = constraint.replace(NEW_ID_PREFIX, "").replace(ID_PREFIX, "");
+ if (description.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
+ description = description.substring(ATTR_LAYOUT_RESOURCE_PREFIX.length());
+ }
+ if (margin > 0) {
+ int dp = state.getRulesEngine().pxToDp(margin);
+ description = String.format("%1$s, margin=%2$d dp", description, dp);
+ }
+ strings.add(description);
+ }
+
+ /** Paints a constraint cycle */
+ void paintCycle(IGraphics gc, GuidelineHandler state, List<Constraint> cycle) {
+ gc.useStyle(DrawingStyle.CYCLE);
+ assert cycle.size() > 0;
+
+ INode from = cycle.get(0).from.node;
+ Rect fromBounds = from.getBounds();
+ if (state.mDraggedNodes.contains(from)) {
+ fromBounds = state.mBounds;
+ }
+ Point fromCenter = fromBounds.center();
+ INode to = null;
+
+ List<Point> points = new ArrayList<Point>();
+ points.add(fromCenter);
+
+ for (Constraint constraint : cycle) {
+ assert constraint.from.node == from;
+ to = constraint.to.node;
+ assert from != null && to != null;
+
+ Point toCenter = to.getBounds().center();
+ points.add(toCenter);
+
+ // Also go through the dragged node bounds
+ boolean isDragged = state.mDraggedNodes.contains(to);
+ if (isDragged) {
+ toCenter = state.mBounds.center();
+ points.add(toCenter);
+ }
+
+ from = to;
+ fromCenter = toCenter;
+ }
+
+ points.add(fromCenter);
+ points.add(points.get(0));
+
+ for (int i = 1, n = points.size(); i < n; i++) {
+ gc.drawLine(points.get(i-1), points.get(i));
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/Match.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/Match.java
new file mode 100644
index 000000000..6f3f0d0f7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/Match.java
@@ -0,0 +1,100 @@
+/*
+ * 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.VALUE_TRUE;
+
+
+import com.android.SdkConstants;
+import static com.android.SdkConstants.ANDROID_URI;
+import com.android.ide.common.api.Segment;
+
+/** A match is a potential pairing of two segments with a given {@link ConstraintType}. */
+class Match {
+ /** the edge of the dragged node that is matched */
+ public final Segment with;
+
+ /** the "other" edge that the dragged edge is matched with */
+ public final Segment edge;
+
+ /** the signed distance between the matched edges */
+ public final int delta;
+
+ /** the type of constraint this is a match for */
+ public final ConstraintType type;
+
+ /** whether this {@link Match} results in a cycle */
+ public boolean cycle;
+
+ /** The associated {@link GuidelineHander} which performed the match */
+ private final GuidelineHandler mHandler;
+
+ /**
+ * Create a new match.
+ *
+ * @param handler the handler which performed the match
+ * @param edge the "other" edge that the dragged edge is matched with
+ * @param with the edge of the dragged node that is matched
+ * @param type the type of constraint this is a match for
+ * @param delta the signed distance between the matched edges
+ */
+ public Match(GuidelineHandler handler, Segment edge, Segment with,
+ ConstraintType type, int delta) {
+ mHandler = handler;
+
+ this.edge = edge;
+ this.with = with;
+ this.type = type;
+ this.delta = delta;
+ }
+
+ /**
+ * Returns the XML constraint attribute value for this match
+ *
+ * @param generateId whether an id should be generated if one is missing
+ * @return the XML constraint attribute value for this match
+ */
+ public String getConstraint(boolean generateId) {
+ if (type.targetParent) {
+ return type.name + '=' + VALUE_TRUE;
+ } else {
+ String id = edge.id;
+ if (id == null || id.length() == -1) {
+ if (!generateId) {
+ // Placeholder to display for the user during dragging
+ id = "<generated>";
+ } else {
+ // Must generate an id on the fly!
+ // See if it's been set by a different constraint we've already applied
+ // to this same node
+ id = edge.node.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id == null || id.length() == 0) {
+ id = mHandler.getRulesEngine().getUniqueId(edge.node.getFqcn());
+ edge.node.setAttribute(ANDROID_URI, ATTR_ID, id);
+ }
+ }
+ }
+ return type.name + '=' + id;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Match [type=" + type + ", delta=" + delta + ", edge=" + edge
+ + "]";
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/MoveHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/MoveHandler.java
new file mode 100644
index 000000000..0fa915d81
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/MoveHandler.java
@@ -0,0 +1,299 @@
+/*
+ * 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.ide.common.api.MarginType.NO_MARGIN;
+import static com.android.ide.common.api.SegmentType.BASELINE;
+import static com.android.ide.common.api.SegmentType.BOTTOM;
+import static com.android.ide.common.api.SegmentType.CENTER_HORIZONTAL;
+import static com.android.ide.common.api.SegmentType.CENTER_VERTICAL;
+import static com.android.ide.common.api.SegmentType.LEFT;
+import static com.android.ide.common.api.SegmentType.RIGHT;
+import static com.android.ide.common.api.SegmentType.TOP;
+import static com.android.SdkConstants.ATTR_ID;
+
+import static java.lang.Math.abs;
+
+import com.android.SdkConstants;
+import static com.android.SdkConstants.ANDROID_URI;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IClientRulesEngine;
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.Segment;
+import com.android.ide.common.layout.BaseLayoutRule;
+import com.android.ide.common.layout.relative.DependencyGraph.ViewData;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A {@link MoveHandler} is a {@link GuidelineHandler} which handles move and drop
+ * gestures, and offers guideline suggestions and snapping.
+ * <p>
+ * Unlike the {@link ResizeHandler}, the {@link MoveHandler} looks for matches for all
+ * different segment types -- the left edge, the right edge, the baseline, the center
+ * edges, and so on -- and picks the best among these.
+ */
+public class MoveHandler extends GuidelineHandler {
+ private int mDraggedBaseline;
+
+ /**
+ * Creates a new {@link MoveHandler}.
+ *
+ * @param layout the layout element the handler is operating on
+ * @param elements the elements being dragged in the move operation
+ * @param rulesEngine the corresponding {@link IClientRulesEngine}
+ */
+ public MoveHandler(INode layout, IDragElement[] elements, IClientRulesEngine rulesEngine) {
+ super(layout, rulesEngine);
+
+ // Compute list of nodes being dragged within the layout, if any
+ List<INode> nodes = new ArrayList<INode>();
+ for (IDragElement element : elements) {
+ ViewData view = mDependencyGraph.getView(element);
+ if (view != null) {
+ nodes.add(view.node);
+ }
+ }
+ mDraggedNodes = nodes;
+
+ mHorizontalDeps = mDependencyGraph.dependsOn(nodes, false /* verticalEdge */);
+ mVerticalDeps = mDependencyGraph.dependsOn(nodes, true /* verticalEdge */);
+
+ for (INode child : layout.getChildren()) {
+ Rect bc = child.getBounds();
+ if (bc.isValid()) {
+ // First see if this node looks like it's the same as one of the
+ // *dragged* bounds
+ boolean isDragged = false;
+ for (IDragElement element : elements) {
+ // This tries to determine if an INode corresponds to an
+ // IDragElement, by comparing their bounds.
+ if (bc.equals(element.getBounds())) {
+ isDragged = true;
+ }
+ }
+
+ if (!isDragged) {
+ String id = child.getStringAttr(ANDROID_URI, ATTR_ID);
+ // It's okay for id to be null; if you apply a constraint
+ // to a node with a missing id we will generate the id
+
+ boolean addHorizontal = !mHorizontalDeps.contains(child);
+ boolean addVertical = !mVerticalDeps.contains(child);
+
+ addBounds(child, id, addHorizontal, addVertical);
+ if (addHorizontal) {
+ addBaseLine(child, id);
+ }
+ }
+ }
+ }
+
+ String id = layout.getStringAttr(ANDROID_URI, ATTR_ID);
+ addBounds(layout, id, true, true);
+ addCenter(layout, id, true, true);
+ }
+
+ @Override
+ protected void snapVertical(Segment vEdge, int x, Rect newBounds) {
+ int maxDistance = BaseLayoutRule.getMaxMatchDistance();
+ if (vEdge.edgeType == LEFT) {
+ int margin = !mSnap ? 0 : abs(newBounds.x - x);
+ if (margin > maxDistance) {
+ mLeftMargin = margin;
+ } else {
+ newBounds.x = x;
+ }
+ } else if (vEdge.edgeType == RIGHT) {
+ int margin = !mSnap ? 0 : abs(newBounds.x - (x - newBounds.w));
+ if (margin > maxDistance) {
+ mRightMargin = margin;
+ } else {
+ newBounds.x = x - newBounds.w;
+ }
+ } else if (vEdge.edgeType == CENTER_VERTICAL) {
+ newBounds.x = x - newBounds.w / 2;
+ } else {
+ assert false : vEdge;
+ }
+ }
+
+ // TODO: Consider unifying this with the snapping logic in ResizeHandler
+ @Override
+ protected void snapHorizontal(Segment hEdge, int y, Rect newBounds) {
+ int maxDistance = BaseLayoutRule.getMaxMatchDistance();
+ if (hEdge.edgeType == TOP) {
+ int margin = !mSnap ? 0 : abs(newBounds.y - y);
+ if (margin > maxDistance) {
+ mTopMargin = margin;
+ } else {
+ newBounds.y = y;
+ }
+ } else if (hEdge.edgeType == BOTTOM) {
+ int margin = !mSnap ? 0 : abs(newBounds.y - (y - newBounds.h));
+ if (margin > maxDistance) {
+ mBottomMargin = margin;
+ } else {
+ newBounds.y = y - newBounds.h;
+ }
+ } else if (hEdge.edgeType == CENTER_HORIZONTAL) {
+ int margin = !mSnap ? 0 : abs(newBounds.y - (y - newBounds.h / 2));
+ if (margin > maxDistance) {
+ mTopMargin = margin;
+ // or bottomMargin?
+ } else {
+ newBounds.y = y - newBounds.h / 2;
+ }
+ } else if (hEdge.edgeType == BASELINE) {
+ newBounds.y = y - mDraggedBaseline;
+ } else {
+ assert false : hEdge;
+ }
+ }
+
+ /**
+ * Updates the handler for the given mouse move
+ *
+ * @param feedback the feedback handler
+ * @param elements the elements being dragged
+ * @param offsetX the new mouse X coordinate
+ * @param offsetY the new mouse Y coordinate
+ * @param modifierMask the keyboard modifiers pressed during the drag
+ */
+ public void updateMove(DropFeedback feedback, IDragElement[] elements,
+ int offsetX, int offsetY, int modifierMask) {
+ mSnap = (modifierMask & DropFeedback.MODIFIER2) == 0;
+
+ Rect firstBounds = elements[0].getBounds();
+ INode firstNode = null;
+ if (mDraggedNodes != null && mDraggedNodes.size() > 0) {
+ // TODO - this isn't quite right; this could be a different node than we have
+ // bounds for!
+ firstNode = mDraggedNodes.iterator().next();
+ firstBounds = firstNode.getBounds();
+ }
+
+ mBounds = new Rect(offsetX, offsetY, firstBounds.w, firstBounds.h);
+ Rect layoutBounds = layout.getBounds();
+ if (mBounds.x2() > layoutBounds.x2()) {
+ mBounds.x -= mBounds.x2() - layoutBounds.x2();
+ }
+ if (mBounds.y2() > layoutBounds.y2()) {
+ mBounds.y -= mBounds.y2() - layoutBounds.y2();
+ }
+ if (mBounds.x < layoutBounds.x) {
+ mBounds.x = layoutBounds.x;
+ }
+ if (mBounds.y < layoutBounds.y) {
+ mBounds.y = layoutBounds.y;
+ }
+
+ clearSuggestions();
+
+ Rect b = mBounds;
+ Segment edge = new Segment(b.y, b.x, b.x2(), null, null, TOP, NO_MARGIN);
+ List<Match> horizontalMatches = findClosest(edge, mHorizontalEdges);
+ edge = new Segment(b.y2(), b.x, b.x2(), null, null, BOTTOM, NO_MARGIN);
+ addClosest(edge, mHorizontalEdges, horizontalMatches);
+
+ edge = new Segment(b.x, b.y, b.y2(), null, null, LEFT, NO_MARGIN);
+ List<Match> verticalMatches = findClosest(edge, mVerticalEdges);
+ edge = new Segment(b.x2(), b.y, b.y2(), null, null, RIGHT, NO_MARGIN);
+ addClosest(edge, mVerticalEdges, verticalMatches);
+
+ // Match center
+ edge = new Segment(b.centerX(), b.y, b.y2(), null, null, CENTER_VERTICAL, NO_MARGIN);
+ addClosest(edge, mCenterVertEdges, verticalMatches);
+ edge = new Segment(b.centerY(), b.x, b.x2(), null, null, CENTER_HORIZONTAL, NO_MARGIN);
+ addClosest(edge, mCenterHorizEdges, horizontalMatches);
+
+ // Match baseline
+ if (firstNode != null) {
+ int baseline = firstNode.getBaseline();
+ if (baseline != -1) {
+ mDraggedBaseline = baseline;
+ edge = new Segment(b.y + baseline, b.x, b.x2(), firstNode, null, BASELINE,
+ NO_MARGIN);
+ addClosest(edge, mHorizontalEdges, horizontalMatches);
+ }
+ } else {
+ int baseline = feedback.dragBaseline;
+ if (baseline != -1) {
+ mDraggedBaseline = baseline;
+ edge = new Segment(offsetY + baseline, b.x, b.x2(), null, null, BASELINE,
+ NO_MARGIN);
+ addClosest(edge, mHorizontalEdges, horizontalMatches);
+ }
+ }
+
+ mHorizontalSuggestions = horizontalMatches;
+ mVerticalSuggestions = verticalMatches;
+ mTopMargin = mBottomMargin = mLeftMargin = mRightMargin = 0;
+
+ Match match = pickBestMatch(mHorizontalSuggestions);
+ if (match != null) {
+ if (mHorizontalDeps.contains(match.edge.node)) {
+ match.cycle = true;
+ }
+
+ // Reset top AND bottom bounds regardless of whether both are bound
+ mMoveTop = true;
+ mMoveBottom = true;
+
+ // TODO: Consider doing the snap logic on all the possible matches
+ // BEFORE sorting, in case this affects the best-pick algorithm (since some
+ // edges snap and others don't).
+ snapHorizontal(match.with, match.edge.at, mBounds);
+
+ if (match.with.edgeType == TOP) {
+ mCurrentTopMatch = match;
+ } else if (match.with.edgeType == BOTTOM) {
+ mCurrentBottomMatch = match;
+ } else {
+ assert match.with.edgeType == CENTER_HORIZONTAL
+ || match.with.edgeType == BASELINE : match.with.edgeType;
+ mCurrentTopMatch = match;
+ }
+ }
+
+ match = pickBestMatch(mVerticalSuggestions);
+ if (match != null) {
+ if (mVerticalDeps.contains(match.edge.node)) {
+ match.cycle = true;
+ }
+
+ // Reset left AND right bounds regardless of whether both are bound
+ mMoveLeft = true;
+ mMoveRight = true;
+
+ snapVertical(match.with, match.edge.at, mBounds);
+
+ if (match.with.edgeType == LEFT) {
+ mCurrentLeftMatch = match;
+ } else if (match.with.edgeType == RIGHT) {
+ mCurrentRightMatch = match;
+ } else {
+ assert match.with.edgeType == CENTER_VERTICAL;
+ mCurrentLeftMatch = match;
+ }
+ }
+
+ checkCycles(feedback);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ResizeHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ResizeHandler.java
new file mode 100644
index 000000000..a5e071d74
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ResizeHandler.java
@@ -0,0 +1,265 @@
+/*
+ * 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.ide.common.api.MarginType.NO_MARGIN;
+import static com.android.ide.common.api.SegmentType.BASELINE;
+import static com.android.ide.common.api.SegmentType.BOTTOM;
+import static com.android.ide.common.api.SegmentType.CENTER_HORIZONTAL;
+import static com.android.ide.common.api.SegmentType.CENTER_VERTICAL;
+import static com.android.ide.common.api.SegmentType.LEFT;
+import static com.android.ide.common.api.SegmentType.RIGHT;
+import static com.android.ide.common.api.SegmentType.TOP;
+import static com.android.SdkConstants.ATTR_ID;
+
+import static java.lang.Math.abs;
+
+import com.android.SdkConstants;
+import static com.android.SdkConstants.ANDROID_URI;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IClientRulesEngine;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.Segment;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.common.layout.BaseLayoutRule;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * A {@link ResizeHandler} is a {@link GuidelineHandler} which handles resizing of individual
+ * edges in a RelativeLayout.
+ */
+public class ResizeHandler extends GuidelineHandler {
+ private final SegmentType mHorizontalEdgeType;
+ private final SegmentType mVerticalEdgeType;
+
+ /**
+ * Creates a new {@link ResizeHandler}
+ *
+ * @param layout the layout containing the resized node
+ * @param resized the node being resized
+ * @param rulesEngine the applicable {@link IClientRulesEngine}
+ * @param horizontalEdgeType the type of horizontal edge being resized, or null
+ * @param verticalEdgeType the type of vertical edge being resized, or null
+ */
+ public ResizeHandler(INode layout, INode resized,
+ IClientRulesEngine rulesEngine,
+ SegmentType horizontalEdgeType, SegmentType verticalEdgeType) {
+ super(layout, rulesEngine);
+
+ assert horizontalEdgeType != null || verticalEdgeType != null;
+ assert horizontalEdgeType != BASELINE && verticalEdgeType != BASELINE;
+ assert horizontalEdgeType != CENTER_HORIZONTAL && verticalEdgeType != CENTER_HORIZONTAL;
+ assert horizontalEdgeType != CENTER_VERTICAL && verticalEdgeType != CENTER_VERTICAL;
+
+ mHorizontalEdgeType = horizontalEdgeType;
+ mVerticalEdgeType = verticalEdgeType;
+
+ Set<INode> nodes = Collections.singleton(resized);
+ mDraggedNodes = nodes;
+
+ mHorizontalDeps = mDependencyGraph.dependsOn(nodes, false /* vertical */);
+ mVerticalDeps = mDependencyGraph.dependsOn(nodes, true /* vertical */);
+
+ if (horizontalEdgeType != null) {
+ if (horizontalEdgeType == TOP) {
+ mMoveTop = true;
+ } else if (horizontalEdgeType == BOTTOM) {
+ mMoveBottom = true;
+ }
+ }
+ if (verticalEdgeType != null) {
+ if (verticalEdgeType == LEFT) {
+ mMoveLeft = true;
+ } else if (verticalEdgeType == RIGHT) {
+ mMoveRight = true;
+ }
+ }
+
+ for (INode child : layout.getChildren()) {
+ if (child != resized) {
+ String id = child.getStringAttr(ANDROID_URI, ATTR_ID);
+ addBounds(child, id,
+ !mHorizontalDeps.contains(child),
+ !mVerticalDeps.contains(child));
+ }
+ }
+
+ addBounds(layout, layout.getStringAttr(ANDROID_URI, ATTR_ID), true, true);
+ }
+
+ @Override
+ protected void snapVertical(Segment vEdge, int x, Rect newBounds) {
+ int maxDistance = BaseLayoutRule.getMaxMatchDistance();
+ if (vEdge.edgeType == LEFT) {
+ int margin = mSnap ? 0 : abs(newBounds.x - x);
+ if (margin > maxDistance) {
+ mLeftMargin = margin;
+ } else {
+ newBounds.w += newBounds.x - x;
+ newBounds.x = x;
+ }
+ } else if (vEdge.edgeType == RIGHT) {
+ int margin = mSnap ? 0 : abs(newBounds.x - (x - newBounds.w));
+ if (margin > maxDistance) {
+ mRightMargin = margin;
+ } else {
+ newBounds.w = x - newBounds.x;
+ }
+ } else {
+ assert false : vEdge;
+ }
+ }
+
+ @Override
+ protected void snapHorizontal(Segment hEdge, int y, Rect newBounds) {
+ int maxDistance = BaseLayoutRule.getMaxMatchDistance();
+ if (hEdge.edgeType == TOP) {
+ int margin = mSnap ? 0 : abs(newBounds.y - y);
+ if (margin > maxDistance) {
+ mTopMargin = margin;
+ } else {
+ newBounds.h += newBounds.y - y;
+ newBounds.y = y;
+ }
+ } else if (hEdge.edgeType == BOTTOM) {
+ int margin = mSnap ? 0 : abs(newBounds.y - (y - newBounds.h));
+ if (margin > maxDistance) {
+ mBottomMargin = margin;
+ } else {
+ newBounds.h = y - newBounds.y;
+ }
+ } else {
+ assert false : hEdge;
+ }
+ }
+
+ @Override
+ protected boolean isEdgeTypeCompatible(SegmentType edge, SegmentType dragged, int delta) {
+ boolean compatible = super.isEdgeTypeCompatible(edge, dragged, delta);
+
+ // When resizing and not snapping (e.g. using margins to pick a specific pixel
+ // width) we cannot use -negative- margins to jump back to a closer edge; we
+ // must always use positive margins, so mark closer edges that result in a negative
+ // margin as not compatible.
+ if (compatible && !mSnap) {
+ switch (dragged) {
+ case LEFT:
+ case TOP:
+ return delta <= 0;
+ default:
+ return delta >= 0;
+ }
+ }
+
+ return compatible;
+ }
+
+ /**
+ * Updates the handler for the given mouse resize
+ *
+ * @param feedback the feedback handler
+ * @param child the node being resized
+ * @param newBounds the new bounds of the resize rectangle
+ * @param modifierMask the keyboard modifiers pressed during the drag
+ */
+ public void updateResize(DropFeedback feedback, INode child, Rect newBounds,
+ int modifierMask) {
+ mSnap = (modifierMask & DropFeedback.MODIFIER2) == 0;
+ mBounds = newBounds;
+ clearSuggestions();
+
+ Rect b = newBounds;
+ Segment hEdge = null;
+ Segment vEdge = null;
+ String childId = child.getStringAttr(ANDROID_URI, ATTR_ID);
+
+ // TODO: MarginType=NO_MARGIN may not be right. Consider resizing a widget
+ // that has margins and how that should be handled.
+
+ if (mHorizontalEdgeType == TOP) {
+ hEdge = new Segment(b.y, b.x, b.x2(), child, childId, mHorizontalEdgeType, NO_MARGIN);
+ } else if (mHorizontalEdgeType == BOTTOM) {
+ hEdge = new Segment(b.y2(), b.x, b.x2(), child, childId, mHorizontalEdgeType,
+ NO_MARGIN);
+ } else {
+ assert mHorizontalEdgeType == null;
+ }
+
+ if (mVerticalEdgeType == LEFT) {
+ vEdge = new Segment(b.x, b.y, b.y2(), child, childId, mVerticalEdgeType, NO_MARGIN);
+ } else if (mVerticalEdgeType == RIGHT) {
+ vEdge = new Segment(b.x2(), b.y, b.y2(), child, childId, mVerticalEdgeType, NO_MARGIN);
+ } else {
+ assert mVerticalEdgeType == null;
+ }
+
+ mTopMargin = mBottomMargin = mLeftMargin = mRightMargin = 0;
+
+ if (hEdge != null && mHorizontalEdges.size() > 0) {
+ // Compute horizontal matches
+ mHorizontalSuggestions = findClosest(hEdge, mHorizontalEdges);
+
+ Match match = pickBestMatch(mHorizontalSuggestions);
+ if (match != null
+ && (!mSnap || Math.abs(match.delta) < BaseLayoutRule.getMaxMatchDistance())) {
+ if (mHorizontalDeps.contains(match.edge.node)) {
+ match.cycle = true;
+ }
+
+ snapHorizontal(hEdge, match.edge.at, newBounds);
+
+ if (hEdge.edgeType == TOP) {
+ mCurrentTopMatch = match;
+ } else if (hEdge.edgeType == BOTTOM) {
+ mCurrentBottomMatch = match;
+ } else {
+ assert hEdge.edgeType == CENTER_HORIZONTAL
+ || hEdge.edgeType == BASELINE : hEdge;
+ mCurrentTopMatch = match;
+ }
+ }
+ }
+
+ if (vEdge != null && mVerticalEdges.size() > 0) {
+ mVerticalSuggestions = findClosest(vEdge, mVerticalEdges);
+
+ Match match = pickBestMatch(mVerticalSuggestions);
+ if (match != null
+ && (!mSnap || Math.abs(match.delta) < BaseLayoutRule.getMaxMatchDistance())) {
+ if (mVerticalDeps.contains(match.edge.node)) {
+ match.cycle = true;
+ }
+
+ // Snap
+ snapVertical(vEdge, match.edge.at, newBounds);
+
+ if (vEdge.edgeType == LEFT) {
+ mCurrentLeftMatch = match;
+ } else if (vEdge.edgeType == RIGHT) {
+ mCurrentRightMatch = match;
+ } else {
+ assert vEdge.edgeType == CENTER_VERTICAL;
+ mCurrentLeftMatch = match;
+ }
+ }
+ }
+
+ checkCycles(feedback);
+ }
+}