diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative')
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); + } +} |