diff options
author | Tor Norbye <tnorbye@google.com> | 2011-04-10 08:18:45 -0700 |
---|---|---|
committer | Tor Norbye <tnorbye@google.com> | 2011-06-01 18:14:32 -0700 |
commit | 80d9301c2e874b29889c41adb0623666cf534fa0 (patch) | |
tree | a05cd9083d275427a83e5957b1763f87309e36ed /eclipse | |
parent | 429ae88878cf781753d8261d350ad89fe5864169 (diff) | |
download | sdk-80d9301c2e874b29889c41adb0623666cf534fa0.zip sdk-80d9301c2e874b29889c41adb0623666cf534fa0.tar.gz sdk-80d9301c2e874b29889c41adb0623666cf534fa0.tar.bz2 |
Resize & Guideline Support
RelativeLayout now has both drop/move and resize guidelines, and
existing constraints are visualized for the selection.
LinearLayout resizing now uses weights to change the size of nodes
rather than setting width/height.
All resize operations offer guidelines to snap to their "wrap_content"
size.
Various bug fixes in related areas as well.
Change-Id: I817e34c6e67ce61cfb137eb067076d91f69f99e9
Diffstat (limited to 'eclipse')
63 files changed, 5024 insertions, 847 deletions
diff --git a/eclipse/dictionary.txt b/eclipse/dictionary.txt index 688004c..4e1279b 100644 --- a/eclipse/dictionary.txt +++ b/eclipse/dictionary.txt @@ -39,6 +39,7 @@ classloader classpath clipboard clipboards +clueless codebase codename codenames @@ -76,9 +77,11 @@ drawable drawables ed editable +endpoint enum enums env +equidistant exec fallback foo @@ -226,17 +229,20 @@ tooltip tooltips traceview translucency +trig typo ui uncomment undescribed undoable +unfiltered unhide unicode uninstall uninstallation uninstalling unset +unweighted upcoming uri url diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DrawingStyle.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DrawingStyle.java index 7317ebc..1f4cd23 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DrawingStyle.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DrawingStyle.java @@ -38,6 +38,17 @@ public enum DrawingStyle { GUIDELINE, /** + * The style used to guideline shadows + */ + GUIDELINE_SHADOW, + + /** + * The style used to draw guidelines, in particular shared edges and center lines; this + * is a dashed edge. + */ + GUIDELINE_DASHED, + + /** * The style used for hovered views (e.g. when the mouse is directly on top * of the view) */ @@ -92,6 +103,19 @@ public enum DrawingStyle { DROP_PREVIEW, /** + * The style used to preview a resize operation. Similar to {@link #DROP_PREVIEW} + * but usually fainter to work better in combination with guidelines which + * are often overlaid during resize. + */ + RESIZE_PREVIEW, + + /** + * The style used to show a proposed resize bound which is being rejected (for example, + * because there is no near edge to attach to in a RelativeLayout). + */ + RESIZE_FAIL, + + /** * The style used to draw help/hint text. */ HELP, @@ -102,6 +126,22 @@ public enum DrawingStyle { INVALID, /** + * The style used to highlight dependencies + */ + DEPENDENCY, + + /** + * The style used to draw an invalid cycle + */ + CYCLE, + + /** + * The style used to highlight the currently dragged views during a layout + * move (if they are not hidden) + */ + DRAGGED, + + /** * The style used to draw empty containers of zero bounds (which are padded * a bit to make them visible during a drag or selection). */ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DropFeedback.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DropFeedback.java index 7ecbe8f..855d8b0 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DropFeedback.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DropFeedback.java @@ -98,6 +98,11 @@ public class DropFeedback { public Rect dragBounds; /** + * The baseline of the primary dragged view. -1 means that the view does not have a baseline. + */ + public int dragBaseline = -1; + + /** * Set to true when the drag'n'drop starts and ends in the same canvas of the * same Eclipse instance. * <p/> @@ -135,4 +140,17 @@ public class DropFeedback { * separators. */ public String errorMessage; + + /** + * A mask of the currently held keyboard modifier keys - some combination of + * {@link #MODIFIER1}, {@link #MODIFIER2}, {@link #MODIFIER3}, or none. + */ + public int modifierMask; + + /** Bitmask value for modifier key 1 (Control on Windows/Linux, Command on Mac, etc) */ + public static final int MODIFIER1 = 1; + /** Bitmask value for modifier key 2 (Shift) */ + public static final int MODIFIER2 = 2; + /** Bitmask value for modifier key 3 (Alt on Windows/Linux, Option on Mac, etc) */ + public static final int MODIFIER3 = 4; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IClientRulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IClientRulesEngine.java index e31323b..b696332 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IClientRulesEngine.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IClientRulesEngine.java @@ -20,12 +20,11 @@ package com.android.ide.common.api; import com.android.annotations.Nullable; import java.util.Collection; +import java.util.Map; /** * A Client Rules Engine is a set of methods that {@link IViewRule}s can use to - * access the client public API of the Rules Engine. Rules can access it via - * the property "_rules_engine" which is dynamically added to {@link IViewRule} - * instances on creation. + * access the client public API of the Rules Engine. * <p> * <b>NOTE: This is not a public or final API; if you rely on this be prepared * to adjust your code for the next tools release.</b> @@ -160,5 +159,56 @@ public interface IClientRulesEngine { * @param nodes the nodes to be selected, never null */ void select(Collection<INode> nodes); + + /** + * Triggers a redraw + */ + void redraw(); + + /** + * Triggers a layout refresh and redraw + */ + void layout(); + + /** + * Converts a pixel to a dp (device independent pixel) for the current screen density + * + * @param px the pixel dimension + * @return the corresponding dp dimension + */ + public int pxToDp(int px); + + /** + * Measure the preferred or actual ("wrap_content") size of the given nodes. + * + * @param parent the parent whose children should be measured + * @param filter a filter to change attributes in the process of measuring, for + * example forcing the layout_width to wrap_content or the layout_weight to + * unset + * @return the corresponding bounds of the nodes + */ + Map<INode, Rect> measureChildren(INode parent, AttributeFilter filter); + + /** + * The {@link AttributeFilter} allows a client of + * {@link IClientRulesEngine#measureChildren} to modify the actual XML values of the + * nodes being rendered, for example to force width and height values to wrap_content + * when measuring preferred size. + */ + public interface AttributeFilter { + /** + * Returns the attribute value for the given node and attribute name. This filter + * allows a client to adjust the attribute values that a node presents to the + * layout library. + * <p> + * Return "" to unset an attribute. Return null to return the unfiltered value. + * + * @param node the node for which the attribute value should be returned + * @param namespace the attribute namespace + * @param localName the attribute local name + * @return an override value, or null to return the unfiltered value + */ + String getAttribute(INode node, String namespace, String localName); + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IGraphics.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IGraphics.java index e037d92..f847694 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IGraphics.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IGraphics.java @@ -45,6 +45,17 @@ public interface IGraphics { void drawLine(Point p1, Point p2); /** + * Draws an arrow from (x1, y1) to (x2, y2). + * + * @param x1 The x coordinate of the beginning of the arrow + * @param y1 The y coordinate of the beginning of the arrow + * @param x2 The x coordinate of the end (point) of the arrow + * @param y2 The y coordinate of the end (point) of the arrow + * @param size The size of the arrowhead + */ + void drawArrow(int x1, int y1, int x2, int y2, int size); + + /** * Draws a rectangle outline between 2 points, using the current foreground * color and alpha. */ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/INode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/INode.java index dd64dfa..e3f34a9 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/INode.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/INode.java @@ -58,6 +58,20 @@ public interface INode { */ Rect getBounds(); + /** + * Returns the margins for this node. + * + * @return the margins for this node, never null + */ + Margins getMargins(); + + /** + * Returns the baseline of this node, or -1 if it has no baseline. + * The baseline is the distance from the top down to the baseline. + * + * @return the baseline, or -1 if not applicable + */ + int getBaseline(); // ---- Hierarchy handling ---- diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewRule.java index 3bcb801..1e37245 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewRule.java @@ -123,6 +123,16 @@ public interface IViewRule { */ List<String> getSelectionHint(INode parentNode, INode childNode); + /** + * Paints any layout-specific selection feedback for the given parent layout. + * + * @param graphics the graphics context to paint into + * @param parentNode the parent layout node + * @param childNodes the child nodes selected in the parent layout + */ + void paintSelectionFeedback(IGraphics graphics, INode parentNode, + List<? extends INode> childNodes); + // ==== Drag'n'drop support ==== /** @@ -218,17 +228,20 @@ public interface IViewRule { */ void onChildInserted(INode child, INode parent, InsertType insertType); - /** * Called by the IDE on the parent layout when a child widget is being resized. This - * is called once at the beginning of the resizing operation. + * is called once at the beginning of the resizing operation. A horizontal edge, + * or a vertical edge, or both, can be resized simultaneously. * * @param child the widget being resized * @param parent the layout containing the child + * @param horizEdge The horizontal edge being resized, or null + * @param verticalEdge the vertical edge being resized, or null * @return a {@link DropFeedback} object which performs an update painter callback * etc. */ - DropFeedback onResizeBegin(INode child, INode parent); + DropFeedback onResizeBegin(INode child, INode parent, + SegmentType horizEdge, SegmentType verticalEdge); /** * Called by the IDE on the parent layout when a child widget is being resized. This @@ -241,8 +254,12 @@ public interface IViewRule { * @param parent the layout containing the child * @param newBounds the new bounds the user has chosen to resize the widget to, * in absolute coordinates + * @param modifierMask The modifier keys currently pressed by the user, as a bitmask + * of the constants {@link DropFeedback#MODIFIER1}, {@link DropFeedback#MODIFIER2} + * and {@link DropFeedback#MODIFIER3}. */ - void onResizeUpdate(DropFeedback feedback, INode child, INode parent, Rect newBounds); + void onResizeUpdate(DropFeedback feedback, INode child, INode parent, Rect newBounds, + int modifierMask); /** * Called by the IDE on the parent layout when a child widget is being resized. This diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Margins.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Margins.java new file mode 100644 index 0000000..9e7c1d9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Margins.java @@ -0,0 +1,56 @@ +/* + * 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.api; + +/** + * Set of margins for a node. + */ +public class Margins { + /** The left margin */ + public final int left; + + /** The right margin */ + public final int right; + + /** The top margin */ + public final int top; + + /** The bottom margin */ + public final int bottom; + + /** + * Creates a new {@link Margins} instance. + * + * @param left the left side margin + * @param right the right side margin + * @param top the top margin + * @param bottom the bottom margin + */ + public Margins(int left, int right, int top, int bottom) { + super(); + this.left = left; + this.right = right; + this.top = top; + this.bottom = bottom; + } + + @Override + public String toString() { + return "Margins [left=" + left + ", right=" + right + ", top=" + top + ", bottom=" + bottom + + "]"; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Rect.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Rect.java index b34d729..f3922e2 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Rect.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Rect.java @@ -130,9 +130,45 @@ public class Rect { y + (h > 0 ? h : 0)); } + /** + * Returns the X coordinate of the right hand side of the rectangle + * + * @return the X coordinate of the right hand side of the rectangle + */ + public int x2() { + return x + w; + } + + /** + * Returns the Y coordinate of the bottom of the rectangle + * + * @return the Y coordinate of the bottom of the rectangle + */ + public int y2() { + return y + h; + } + + /** + * Returns the X coordinate of the center of the rectangle + * + * @return the X coordinate of the center of the rectangle + */ + public int centerX() { + return x + w / 2; + } + + /** + * Returns the Y coordinate of the center of the rectangle + * + * @return the Y coordinate of the center of the rectangle + */ + public int centerY() { + return y + h / 2; + } + @Override public String toString() { - return String.format("Rect [%dx%d - %dx%d]", x, y, w, h); + return String.format("Rect [(%d,%d)-(%d,%d): %dx%d]", x, y, x + w, y + h, w, h); } @Override @@ -162,4 +198,13 @@ public class Rect { hc ^= ((h >> 24) & 0x00000FF) | ((h & 0x0FFFFFF) << 8); return hc; } + + /** + * Returns the center point in the rectangle + * + * @return the center point in the rectangle + */ + public Point center() { + return new Point(x + w / 2, y + h / 2); + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Segment.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Segment.java new file mode 100644 index 0000000..845f82d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Segment.java @@ -0,0 +1,69 @@ +/* + * 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.api; + +import com.android.ide.common.layout.relative.MarginType; + +/** + * A segment is a straight horizontal or vertical line between two points, typically an + * edge of a node but also possibly some internal segment like a baseline or a center + * line, and it can be offset by a margin from the node's visible bounds. + */ +public class Segment { + /** For horizontal lines, the y coordinate; for vertical lines the x */ + public final int at; + + /** The starting coordinate along the line */ + public final int from; + + /** The ending coordinate along the line */ + public final int to; + + /** Whether the edge is a top edge, a baseline edge, a left edge, etc */ + public final SegmentType edgeType; + + /** + * Whether the edge is offset from the node by a margin or not, or whether it has no + * margin + */ + public final MarginType marginType; + + /** The node that contains this edge */ + public final INode node; + + /** The id of the node */ + public final String id; + + public Segment(int at, int from, int to, INode node, String id, SegmentType edgeType, + MarginType marginType) { + this.at = at; + this.from = from; + this.to = to; + this.node = node; + this.id = id; + this.edgeType = edgeType; + this.marginType = marginType; + } + + @Override + public String toString() { + String nodeStr = node == null ? "null" : node.getFqcn().substring( + node.getFqcn().lastIndexOf(('.')) + 1); + return "Segment [edgeType=" + edgeType + ", node=" + nodeStr + ", at=" + at + ", id=" + id + + ", from=" + from + ", to=" + to + ", marginType=" + marginType + "]"; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/SegmentType.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/SegmentType.java new file mode 100644 index 0000000..a21247d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/SegmentType.java @@ -0,0 +1,93 @@ +/* + * 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.api; + +/** A segment type describes the different roles or positions a segment can have in a node */ +public enum SegmentType { + LEFT, TOP, RIGHT, BOTTOM, BASELINE, CENTER_VERTICAL, CENTER_HORIZONTAL, UNKNOWN; + + public boolean isHorizontal() { + return this == TOP || this == BOTTOM || this == BASELINE || this == CENTER_HORIZONTAL; + } + + /** + * Returns the X coordinate for an edge of this type given its bounds + * + * @param node the node containing the edge + * @param bounds the bounds of the node + * @return the X coordinate for an edge of this type given its bounds + */ + public int getX(INode node, Rect bounds) { + // We pass in the bounds rather than look it up via node.getBounds() because + // during a resize or move operation, we call this method to look up proposed + // bounds rather than actual bounds + switch (this) { + case RIGHT: + return bounds.x + bounds.w; + case TOP: + case BOTTOM: + case CENTER_VERTICAL: + return bounds.x + bounds.w / 2; + case UNKNOWN: + assert false; + return bounds.x; + case LEFT: + case BASELINE: + default: + return bounds.x; + } + } + + /** + * Returns the Y coordinate for an edge of this type given its bounds + * + * @param node the node containing the edge + * @param bounds the bounds of the node + * @return the Y coordinate for an edge of this type given its bounds + */ + public int getY(INode node, Rect bounds) { + switch (this) { + case TOP: + return bounds.y; + case BOTTOM: + return bounds.y + bounds.h; + case BASELINE: { + int baseline = node != null ? node.getBaseline() : -1; + if (node == null) { + // This happens when you are dragging an element and we don't have + // a node (only an IDragElement) such as on a palette drag. + // For now just hack it. + baseline = (int) (bounds.h * 0.8f); // HACK + } + return bounds.y + baseline; + } + case UNKNOWN: + assert false; + return bounds.y; + case RIGHT: + case LEFT: + case CENTER_HORIZONTAL: + default: + return bounds.y + bounds.h / 2; + } + } + + @Override + public String toString() { + return name(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java index b42e64e..111434f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java @@ -20,6 +20,7 @@ import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_X; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_Y; import static com.android.ide.common.layout.LayoutConstants.VALUE_N_DP; +import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; import com.android.ide.common.api.DrawingStyle; import com.android.ide.common.api.DropFeedback; @@ -31,6 +32,7 @@ import com.android.ide.common.api.INodeHandler; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; +import com.android.ide.common.api.SegmentType; import com.android.util.Pair; import java.util.ArrayList; @@ -213,29 +215,33 @@ public class AbsoluteLayoutRule extends BaseLayoutRule { * this case, the bottom right corner will stay fixed). */ @Override - protected void setNewSizeBounds(INode node, Rect previousBounds, Rect newBounds) { - super.setNewSizeBounds(node, previousBounds, newBounds); - if (newBounds.x != previousBounds.x) { + protected void setNewSizeBounds(ResizeState resizeState, INode node, INode layout, + Rect previousBounds, Rect newBounds, SegmentType horizontalEdge, + SegmentType verticalEdge) { + super.setNewSizeBounds(resizeState, node, layout, previousBounds, newBounds, + horizontalEdge, verticalEdge); + if (verticalEdge != null && newBounds.x != previousBounds.x) { node.setAttribute(ANDROID_URI, ATTR_LAYOUT_X, - String.format(VALUE_N_DP, newBounds.x -node.getParent().getBounds().x)); + String.format(VALUE_N_DP, + mRulesEngine.pxToDp(newBounds.x - node.getParent().getBounds().x))); } - if (newBounds.y != previousBounds.y) { + if (horizontalEdge != null && newBounds.y != previousBounds.y) { node.setAttribute(ANDROID_URI, ATTR_LAYOUT_Y, - String.format(VALUE_N_DP, newBounds.y - node.getParent().getBounds().y)); + String.format(VALUE_N_DP, + mRulesEngine.pxToDp(newBounds.y - node.getParent().getBounds().y))); } } - // Overridden so we can change the drag feedback message; the super implementation - // only shows the width and height, and we want to include the new position as well @Override - public void onResizeUpdate(DropFeedback feedback, INode child, INode parent, - Rect newBounds) { - super.onResizeUpdate(feedback, child, parent, newBounds); + protected String getResizeUpdateMessage(ResizeState resizeState, INode child, INode parent, + Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { Rect parentBounds = parent.getBounds(); - feedback.message = String.format("Set bounds to (x = %d, y = %d, width = %d, height = %d)", - newBounds.x - parentBounds.x, newBounds.y - parentBounds.y, - newBounds.w, newBounds.h); + return String.format("Set bounds to (x = %d, y = %d, width = %s, height = %s)", + mRulesEngine.pxToDp(newBounds.x - parentBounds.x), + mRulesEngine.pxToDp(newBounds.y - parentBounds.y), + resizeState.wrapWidth ? + VALUE_WRAP_CONTENT : Integer.toString(mRulesEngine.pxToDp(newBounds.w)), + resizeState.wrapHeight ? + VALUE_WRAP_CONTENT : Integer.toString(mRulesEngine.pxToDp(newBounds.h))); } - - } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java index bd6c806..4e6ea98 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java @@ -28,11 +28,15 @@ import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH; import static com.android.ide.common.layout.LayoutConstants.ATTR_TEXT; import static com.android.ide.common.layout.LayoutConstants.VALUE_FILL_PARENT; import static com.android.ide.common.layout.LayoutConstants.VALUE_MATCH_PARENT; +import static com.android.ide.common.layout.LayoutConstants.VALUE_N_DP; import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; +import com.android.ide.common.api.DrawingStyle; import com.android.ide.common.api.DropFeedback; import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.common.api.IClientRulesEngine; import com.android.ide.common.api.IDragElement; +import com.android.ide.common.api.IFeedbackPainter; import com.android.ide.common.api.IGraphics; import com.android.ide.common.api.IMenuCallback; import com.android.ide.common.api.INode; @@ -40,9 +44,11 @@ import com.android.ide.common.api.INodeHandler; import com.android.ide.common.api.MenuAction; 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.api.IAttributeInfo.Format; import com.android.ide.common.api.IDragElement.IDragAttribute; import com.android.ide.common.api.MenuAction.ChoiceProvider; +import com.android.sdklib.SdkConstants; import com.android.util.Pair; import java.net.URL; @@ -566,4 +572,233 @@ public class BaseLayoutRule extends BaseViewRule { } }); } + + // ---- Resizing ---- + + /** State held during resizing operations */ + protected static class ResizeState { + /** The proposed resized bounds of the node */ + public Rect bounds; + + /** The preferred wrap_content bounds of the node */ + public Rect wrapBounds; + + /** The type of horizontal edge being resized, or null */ + public SegmentType horizontalEdgeType; + + /** The type of vertical edge being resized, or null */ + public SegmentType verticalEdgeType; + + /** Whether the user has snapped to the wrap_content width */ + public boolean wrapWidth; + + /** Whether the user has snapped to the wrap_content height */ + public boolean wrapHeight; + } + + @Override + public DropFeedback onResizeBegin(INode child, INode parent, + SegmentType horizontalEdge, SegmentType verticalEdge) { + ResizeState state = new ResizeState(); + state.horizontalEdgeType = horizontalEdge; + state.verticalEdgeType = verticalEdge; + + // Compute preferred (wrap_content) size such that we can offer guidelines to + // snap to the preferred size + Map<INode, Rect> sizes = mRulesEngine.measureChildren(parent, + new IClientRulesEngine.AttributeFilter() { + public String getAttribute(INode node, String namespace, String localName) { + // Change attributes to wrap_content + if (ATTR_LAYOUT_WIDTH.equals(localName) + && SdkConstants.NS_RESOURCES.equals(namespace)) { + return VALUE_WRAP_CONTENT; + } + if (ATTR_LAYOUT_HEIGHT.equals(localName) + && SdkConstants.NS_RESOURCES.equals(namespace)) { + return VALUE_WRAP_CONTENT; + } + + return null; + } + }); + if (sizes != null) { + state.wrapBounds = sizes.get(child); + } + + return new DropFeedback(state, new IFeedbackPainter() { + public void paint(IGraphics gc, INode node, DropFeedback feedback) { + ResizeState resizeState = (ResizeState) feedback.userData; + if (resizeState != null && resizeState.bounds != null) { + gc.useStyle(DrawingStyle.RESIZE_PREVIEW); + Rect b = resizeState.bounds; + gc.drawRect(b); + + if (resizeState.wrapBounds != null) { + gc.useStyle(DrawingStyle.GUIDELINE); + int wrapWidth = resizeState.wrapBounds.w; + int wrapHeight = resizeState.wrapBounds.h; + + // Show the "wrap_content" guideline. + // If we are showing both the wrap_width and wrap_height lines + // then we show at most the rectangle formed by the two lines; + // otherwise we show the entire width of the line + if (resizeState.horizontalEdgeType != null) { + int y = -1; + switch (resizeState.horizontalEdgeType) { + case TOP: + y = b.y + b.h - wrapHeight; + break; + case BOTTOM: + y = b.y + wrapHeight; + break; + default: assert false : resizeState.horizontalEdgeType; + } + if (resizeState.verticalEdgeType != null) { + switch (resizeState.verticalEdgeType) { + case LEFT: + gc.drawLine(b.x + b.w - wrapWidth, y, b.x + b.w, y); + break; + case RIGHT: + gc.drawLine(b.x, y, b.x + wrapWidth, y); + break; + default: assert false : resizeState.verticalEdgeType; + } + } else { + gc.drawLine(b.x, y, b.x + b.w, y); + } + } + if (resizeState.verticalEdgeType != null) { + int x = -1; + switch (resizeState.verticalEdgeType) { + case LEFT: + x = b.x + b.w - wrapWidth; + break; + case RIGHT: + x = b.x + wrapWidth; + break; + default: assert false : resizeState.verticalEdgeType; + } + if (resizeState.horizontalEdgeType != null) { + switch (resizeState.horizontalEdgeType) { + case TOP: + gc.drawLine(x, b.y + b.h - wrapHeight, x, b.y + b.h); + break; + case BOTTOM: + gc.drawLine(x, b.y, x, b.y + wrapHeight); + break; + default: assert false : resizeState.horizontalEdgeType; + } + } else { + gc.drawLine(x, b.y, x, b.y + b.h); + } + } + } + } + } + }); + } + + public static final int getMaxMatchDistance() { + // TODO - make constant once we're happy with the feel + return 20; + } + + + @Override + public void onResizeUpdate(DropFeedback feedback, INode child, INode parent, + Rect newBounds, int modifierMask) { + ResizeState state = (ResizeState) feedback.userData; + state.bounds = newBounds; + + // Match on wrap bounds + state.wrapWidth = state.wrapHeight = false; + if (state.wrapBounds != null) { + Rect b = state.wrapBounds; + int maxMatchDistance = getMaxMatchDistance(); + if (state.horizontalEdgeType != null) { + if (Math.abs(newBounds.h - b.h) < maxMatchDistance) { + state.wrapHeight = true; + if (state.horizontalEdgeType == SegmentType.TOP) { + newBounds.y += newBounds.h - b.h; + } + newBounds.h = b.h; + } + } + if (state.verticalEdgeType != null) { + if (Math.abs(newBounds.w - b.w) < maxMatchDistance) { + state.wrapWidth = true; + if (state.verticalEdgeType == SegmentType.LEFT) { + newBounds.x += newBounds.w - b.w; + } + newBounds.w = b.w; + } + } + } + + feedback.message = getResizeUpdateMessage(state, child, parent, + newBounds, state.horizontalEdgeType, state.verticalEdgeType); + } + + @Override + public void onResizeEnd(DropFeedback feedback, INode child, final INode parent, + final Rect newBounds) { + final Rect oldBounds = child.getBounds(); + if (oldBounds.w != newBounds.w || oldBounds.h != newBounds.h) { + final ResizeState state = (ResizeState) feedback.userData; + child.editXml("Resize", new INodeHandler() { + public void handle(INode n) { + setNewSizeBounds(state, n, parent, oldBounds, newBounds, + state.horizontalEdgeType, state.verticalEdgeType); + } + }); + } + } + + /** + * Returns the message to display to the user during the resize operation + * + * @param resizeState the current resize state + * @param child the child node being resized + * @param parent the parent of the resized node + * @param newBounds the new bounds to resize the child to, in pixels + * @param horizontalEdge the horizontal edge being resized + * @param verticalEdge the vertical edge being resized + * @return the message to display for the current resize bounds + */ + protected String getResizeUpdateMessage(ResizeState resizeState, INode child, INode parent, + Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { + String width = resizeState.wrapWidth ? VALUE_WRAP_CONTENT : + String.format(VALUE_N_DP, mRulesEngine.pxToDp(newBounds.w)); + String height = resizeState.wrapHeight ? VALUE_WRAP_CONTENT : + String.format(VALUE_N_DP, mRulesEngine.pxToDp(newBounds.h)); + + // U+00D7: Unicode for multiplication sign + return String.format("Resize to %s \u00D7 %s", width, height); + } + + /** + * Performs the edit on the node to complete a resizing operation. The actual edit + * part is pulled out such that subclasses can change/add to the edits and be part of + * the same undo event + * + * @param resizeState the current resize state + * @param node the child node being resized + * @param layout the parent of the resized node + * @param newBounds the new bounds to resize the child to, in pixels + * @param horizontalEdge the horizontal edge being resized + * @param verticalEdge the vertical edge being resized + */ + protected void setNewSizeBounds(ResizeState resizeState, INode node, INode layout, + Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { + if (verticalEdge != null && (newBounds.w != oldBounds.w || resizeState.wrapWidth)) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, + resizeState.wrapWidth ? VALUE_WRAP_CONTENT : + String.format(VALUE_N_DP, mRulesEngine.pxToDp(newBounds.w))); + } + if (horizontalEdge != null && (newBounds.h != oldBounds.h || resizeState.wrapHeight)) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, + resizeState.wrapHeight ? VALUE_WRAP_CONTENT : + String.format(VALUE_N_DP, mRulesEngine.pxToDp(newBounds.h))); + } + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java index 58329e6..920aaf3 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java @@ -25,15 +25,12 @@ import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX; import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; import static com.android.ide.common.layout.LayoutConstants.VALUE_FILL_PARENT; import static com.android.ide.common.layout.LayoutConstants.VALUE_MATCH_PARENT; -import static com.android.ide.common.layout.LayoutConstants.VALUE_N_DP; import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; -import com.android.ide.common.api.DrawingStyle; import com.android.ide.common.api.DropFeedback; import com.android.ide.common.api.IAttributeInfo; import com.android.ide.common.api.IClientRulesEngine; import com.android.ide.common.api.IDragElement; -import com.android.ide.common.api.IFeedbackPainter; import com.android.ide.common.api.IGraphics; import com.android.ide.common.api.IMenuCallback; import com.android.ide.common.api.INode; @@ -44,6 +41,7 @@ import com.android.ide.common.api.InsertType; import com.android.ide.common.api.MenuAction; 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.api.IAttributeInfo.Format; import java.util.ArrayList; @@ -298,7 +296,7 @@ public class BaseViewRule implements IViewRule { defaultValue = ""; } String value = mRulesEngine.displayInput( - "Set custom layout attribute value (example: 50dip)", + "Set custom layout attribute value (example: 50dp)", defaultValue, null); if (value != null && value.trim().length() > 0) { return value.trim(); @@ -665,10 +663,12 @@ public class BaseViewRule implements IViewRule { } public static String stripIdPrefix(String id) { - if (id.startsWith(NEW_ID_PREFIX)) { - id = id.substring(NEW_ID_PREFIX.length()); + if (id == null) { + return ""; //$NON-NLS-1$ + } else if (id.startsWith(NEW_ID_PREFIX)) { + return id.substring(NEW_ID_PREFIX.length()); } else if (id.startsWith(ID_PREFIX)) { - id = id.substring(ID_PREFIX.length()); + return id.substring(ID_PREFIX.length()); } return id; } @@ -680,53 +680,22 @@ public class BaseViewRule implements IViewRule { return value; } - // ---- Resizing ---- - - public DropFeedback onResizeBegin(INode child, INode parent) { - return new DropFeedback(null, new IFeedbackPainter() { - public void paint(IGraphics gc, INode node, DropFeedback feedback) { - Rect r = (Rect) feedback.userData; - if (r != null) { - gc.useStyle(DrawingStyle.DROP_PREVIEW); - gc.drawRect(r); - } - } - }); + public void paintSelectionFeedback(IGraphics graphics, INode parentNode, + List<? extends INode> childNodes) { } - public void onResizeUpdate(DropFeedback feedback, INode child, INode parent, - Rect newBounds) { - feedback.userData = newBounds; - feedback.message = String.format("Resize to %d x %d dip", newBounds.w, newBounds.h); + // ---- Resizing ---- - // TODO: Guidelines + public DropFeedback onResizeBegin(INode child, INode parent, SegmentType horizontalEdge, + SegmentType verticalEdge) { + return null; } - public void onResizeEnd(DropFeedback feedback, INode child, INode parent, - final Rect newBounds) { - final Rect oldBounds = child.getBounds(); - if (oldBounds.w != newBounds.w || oldBounds.h != newBounds.h) { - child.editXml("Resize", new INodeHandler() { - public void handle(INode n) { - setNewSizeBounds(n, oldBounds, newBounds); - } - }); - } + public void onResizeUpdate(DropFeedback feedback, INode child, INode parent, Rect newBounds, + int modifierMask) { } - /** - * Performs the edit on the node to complete a resizing operation. The actual edit - * part is pulled out such that subclasses can change/add to the edits and be part of - * the same undo event - */ - protected void setNewSizeBounds(INode node, Rect oldBounds, Rect newBounds) { - if (newBounds.w != oldBounds.w) { - node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, - String.format(VALUE_N_DP, newBounds.w)); - } - if (newBounds.h != oldBounds.h) { - node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, - String.format(VALUE_N_DP, newBounds.h)); - } + public void onResizeEnd(DropFeedback feedback, INode child, final INode parent, + final Rect newBounds) { } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java index 20379a6..ae82559 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java @@ -90,6 +90,7 @@ public class LayoutConstants { public static final String ATTR_LAYOUT_ALIGN_BASELINE = "layout_alignBaseline"; //$NON-NLS-1$ + public static final String ATTR_LAYOUT_CENTER_IN_PARENT = "layout_centerInParent"; //$NON-NLS-1$ public static final String ATTR_LAYOUT_CENTER_VERTICAL = "layout_centerVertical"; //$NON-NLS-1$ public static final String ATTR_LAYOUT_CENTER_HORIZONTAL = "layout_centerHorizontal"; //$NON-NLS-1$ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java index 4b9d006..4b13b84 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java @@ -25,10 +25,14 @@ import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH; import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION; import static com.android.ide.common.layout.LayoutConstants.ATTR_WEIGHT_SUM; import static com.android.ide.common.layout.LayoutConstants.VALUE_HORIZONTAL; +import static com.android.ide.common.layout.LayoutConstants.VALUE_N_DP; import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL; +import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; +import com.android.annotations.VisibleForTesting; import com.android.ide.common.api.DrawingStyle; 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.IFeedbackPainter; import com.android.ide.common.api.IGraphics; @@ -41,14 +45,18 @@ import com.android.ide.common.api.InsertType; import com.android.ide.common.api.MenuAction; 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.api.IViewMetadata.FillPreference; import com.android.ide.common.api.MenuAction.OrderedChoices; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.sdklib.SdkConstants; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; /** * An {@link IViewRule} for android.widget.LinearLayout and all its derived @@ -213,12 +221,7 @@ public class LinearLayoutRule extends BaseLayoutRule { } else { share = sum / numTargets; } - String value; - if (share != (int) share) { - value = String.format("%.2f", (float) share); //$NON-NLS-1$ - } else { - value = Integer.toString((int) share); - } + String value = formatFloatAttribute((float) share); for (INode target : targets) { target.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, value); } @@ -681,4 +684,254 @@ public class LinearLayoutRule extends BaseLayoutRule { return mInsertPos == mNumPositions - 1; } } + + @Override + public DropFeedback onResizeBegin(INode child, INode parent, SegmentType horizontalEdge, + SegmentType verticalEdge) { + return super.onResizeBegin(child, parent, horizontalEdge, verticalEdge); + } + + /** + * {@inheritDoc} + * <p> + * Overridden in this layout in order to make resizing affect the layout_weight + * attribute instead of the layout_width (for horizontal LinearLayouts) or + * layout_height (for vertical LinearLayouts). + */ + @Override + protected void setNewSizeBounds(ResizeState resizeState, INode node, INode layout, + Rect previousBounds, Rect newBounds, SegmentType horizontalEdge, + SegmentType verticalEdge) { + final Rect oldBounds = node.getBounds(); + if (oldBounds.equals(newBounds)) { + return; + } + // Handle resizing in the opposite dimension of the layout + boolean isVertical = isVertical(layout); + if (!isVertical && horizontalEdge != null) { + if (newBounds.h != oldBounds.h || resizeState.wrapHeight) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, + resizeState.wrapHeight ? VALUE_WRAP_CONTENT : + String.format(VALUE_N_DP, mRulesEngine.pxToDp(newBounds.h))); + } + if (verticalEdge == null) { + return; + } + // else: fall through to compute a dynamic weight + } + if (isVertical && verticalEdge != null) { + if (newBounds.w != oldBounds.w || resizeState.wrapWidth) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, + resizeState.wrapWidth ? VALUE_WRAP_CONTENT : + String.format(VALUE_N_DP, mRulesEngine.pxToDp(newBounds.w))); + } + if (horizontalEdge == null) { + return; + } + } + + // If we're setting the width/height to wrap_content in the dimension of the + // linear layout, then just apply wrap_content and clear weights. + if (!isVertical && verticalEdge != null) { + if (resizeState.wrapWidth) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT); + // Clear weight + if (getWeight(node) > 0.0f) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null); + } + return; + } + if (newBounds.w == oldBounds.w) { + return; + } + } + + if (isVertical && horizontalEdge != null) { + if (resizeState.wrapHeight) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT); + // Clear weight + if (getWeight(node) > 0.0f) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null); + } + return; + } + if (newBounds.h == oldBounds.h) { + return; + } + } + + float sum = getWeightSum(layout); + if (sum <= 0.0f) { + sum = 1.0f; + layout.setAttribute(ANDROID_URI, ATTR_WEIGHT_SUM, formatFloatAttribute(sum)); + } + + Map<INode, Rect> sizes = mRulesEngine.measureChildren(layout, + new IClientRulesEngine.AttributeFilter() { + public String getAttribute(INode n, String namespace, String localName) { + // Clear out layout weights; we need to measure the unweighted sizes + // of the children + if (ATTR_LAYOUT_WEIGHT.equals(localName) + && SdkConstants.NS_RESOURCES.equals(namespace)) { + return ""; //$NON-NLS-1$ + } + + return null; + } + }); + int totalLength = 0; + for (Map.Entry<INode, Rect> entry : sizes.entrySet()) { + Rect preferredSize = entry.getValue(); + if (isVertical) { + totalLength += preferredSize.h; + } else { + totalLength += preferredSize.w; + } + } + + Rect layoutBounds = layout.getBounds(); + int remaining = (isVertical ? layoutBounds.h : layoutBounds.w) - totalLength; + Rect nodeBounds = sizes.get(node); + if (nodeBounds == null) { + super.setNewSizeBounds(resizeState, node, layout, oldBounds, newBounds, horizontalEdge, + verticalEdge); + return; + } + assert nodeBounds != null; + + if (remaining > 0) { + int missing = 0; + if (isVertical) { + if (newBounds.h > nodeBounds.h) { + missing = newBounds.h - nodeBounds.h; + } else if (newBounds.h > resizeState.wrapBounds.h) { + // The weights concern how much space to ADD to the view. + // What if we have resized it to a size *smaller* than its current + // size without the weight delta? This can happen if you for example + // have set a hardcoded size, such as 500dp, and then size it to some + // smaller size. + missing = newBounds.h - resizeState.wrapBounds.h; + remaining += nodeBounds.h - resizeState.wrapBounds.h; + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT); + } + } else { + if (newBounds.w > nodeBounds.w) { + missing = newBounds.w - nodeBounds.w; + } else if (newBounds.w > resizeState.wrapBounds.w) { + missing = newBounds.w - resizeState.wrapBounds.w; + remaining += nodeBounds.w - resizeState.wrapBounds.w; + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT); + } + } + if (missing > 0) { + // (weight / weightSum) * remaining = missing, so + // weight = missing * weightSum / remaining + float weight = missing * sum / remaining; + String value = weight > 0 ? formatFloatAttribute(weight) : null; + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, value); + } + } else { + // TODO: This algorithm should be refined. + // One possible solution is to clear the weights and sizes of all children + // to the left or right of the resized node (depending on whether the right + // or left edge was resized - the key point being that the other edge should + // not move). + + // There is no leftover space after adding up the wrap-content sizes of the + // children. In that case, just make the weight of this child the same proportion + // of the sum-of-weights as its new size is out of the parent size. + + // Use actual sum of weights, not the declared sum on the parent layout, + // to get the proportions right + float otherSum = 0.0f; + for (INode child : layout.getChildren()) { + if (child != node) { + otherSum += getWeight(child); + } + } + + float newSize = isVertical ? newBounds.h : newBounds.w; + float totalSize = isVertical ? layoutBounds.h : layoutBounds.w; + float weight; + if (newSize >= totalSize) { + // The new view was resized to something larger than the layout itself; + // that obviously can't be achieved with layout weights, so just pick + // something large to give it a lot of space but not all. + weight = 10 * otherSum; + } else { + weight = newSize * otherSum / (totalSize - newSize); + } + String value = weight > 0 ? formatFloatAttribute(weight) : null; + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, value); + String fill = getFillParentValueName(); + node.setAttribute(ANDROID_URI, isVertical ? ATTR_LAYOUT_WEIGHT : ATTR_LAYOUT_WIDTH, + fill); + } + } + + @Override + protected String getResizeUpdateMessage(ResizeState resizeState, INode child, INode parent, + Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { + return super.getResizeUpdateMessage(resizeState, child, parent, newBounds, + horizontalEdge, verticalEdge); + // TODO: Change message to display the current layout weight instead + } + + /** + * Returns the layout weight of of the given child of a LinearLayout, or 0.0 if it + * does not define a weight + */ + private static float getWeight(INode linearLayoutChild) { + String weight = linearLayoutChild.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WEIGHT); + if (weight != null && weight.length() > 0) { + try { + return Float.parseFloat(weight); + } catch (NumberFormatException nfe) { + AdtPlugin.log(nfe, "Invalid weight %1$s", weight); + } + } + + return 0.0f; + } + + /** + * Returns the sum of all the layout weights of the children in the given LinearLayout + * + * @param linearLayout the layout to compute the total sum for + * @return the total sum of all the layout weights in the given layout + */ + private static float getWeightSum(INode linearLayout) { + String weightSum = linearLayout.getStringAttr(ANDROID_URI, + ATTR_WEIGHT_SUM); + float sum = -1.0f; + if (weightSum != null) { + // Distribute + try { + sum = Float.parseFloat(weightSum); + return sum; + } catch (NumberFormatException nfe) { + // Just keep using the default + } + } + + return getSumOfWeights(linearLayout); + } + + private static float getSumOfWeights(INode linearLayout) { + float sum = 0.0f; + for (INode child : linearLayout.getChildren()) { + sum += getWeight(child); + } + + return sum; + } + + @VisibleForTesting + static String formatFloatAttribute(float value) { + if (value != (int) value) { + return String.format("%.2f", value); //$NON-NLS-1$ + } else { + return Integer.toString((int) value); + } + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java index 33fe202..17f0f8b 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java @@ -18,8 +18,23 @@ package com.android.ide.common.layout; import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; import static com.android.ide.common.layout.LayoutConstants.ATTR_GRAVITY; -import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ABOVE; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BOTTOM; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_LEFT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_RIGHT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_TOP; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_IN_PARENT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF; import static com.android.ide.common.layout.LayoutConstants.VALUE_ABOVE; import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_BASELINE; import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_BOTTOM; @@ -36,37 +51,54 @@ import static com.android.ide.common.layout.LayoutConstants.VALUE_CENTER_HORIZON import static com.android.ide.common.layout.LayoutConstants.VALUE_CENTER_IN_PARENT; import static com.android.ide.common.layout.LayoutConstants.VALUE_CENTER_VERTICAL; import static com.android.ide.common.layout.LayoutConstants.VALUE_TO_LEFT_OF; +import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE; import static com.android.ide.common.layout.LayoutConstants.VAUE_TO_RIGHT_OF; -import com.android.ide.common.api.DrawingStyle; import com.android.ide.common.api.DropFeedback; -import com.android.ide.common.api.IAttributeInfo; import com.android.ide.common.api.IDragElement; -import com.android.ide.common.api.IFeedbackPainter; import com.android.ide.common.api.IGraphics; +import com.android.ide.common.api.IMenuCallback; import com.android.ide.common.api.INode; import com.android.ide.common.api.INodeHandler; import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.InsertType; import com.android.ide.common.api.MenuAction; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; -import com.android.ide.common.api.IAttributeInfo.Format; -import com.android.ide.common.api.INode.IAttribute; +import com.android.ide.common.api.SegmentType; +import com.android.ide.common.layout.relative.ConstraintPainter; +import com.android.ide.common.layout.relative.GuidelinePainter; +import com.android.ide.common.layout.relative.MoveHandler; +import com.android.ide.common.layout.relative.ResizeHandler; import com.android.util.Pair; +import java.net.URL; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; /** * An {@link IViewRule} for android.widget.RelativeLayout and all its derived * classes. */ public class RelativeLayoutRule extends BaseLayoutRule { + private static final String ACTION_SHOW_STRUCTURE = "_structure"; //$NON-NLS-1$ + private static final String ACTION_SHOW_CONSTRAINTS = "_constraints"; //$NON-NLS-1$ + private static final String ACTION_CENTER_VERTICAL = "_centerVert"; //$NON-NLS-1$ + private static final String ACTION_CENTER_HORIZONTAL = "_centerHoriz"; //$NON-NLS-1$ + private static final URL ICON_CENTER_VERTICALLY = + RelativeLayoutRule.class.getResource("centerVertically.png"); //$NON-NLS-1$ + private static final URL ICON_CENTER_HORIZONTALLY = + RelativeLayoutRule.class.getResource("centerHorizontally.png"); //$NON-NLS-1$ + private static final URL ICON_SHOW_STRUCTURE = + BaseLayoutRule.class.getResource("structure.png"); //$NON-NLS-1$ + private static final URL ICON_SHOW_CONSTRAINTS = + BaseLayoutRule.class.getResource("constraints.png"); //$NON-NLS-1$ + + public static boolean sShowStructure = false; + public static boolean sShowConstraints = true; // ==== Selection ==== @@ -102,526 +134,67 @@ public class RelativeLayoutRule extends BaseLayoutRule { } } - // ==== Drag'n'drop support ==== - @Override - public DropFeedback onDropEnter(INode targetNode, final IDragElement[] elements) { - - if (elements.length == 0) { - return null; + public void paintSelectionFeedback(IGraphics graphics, INode parentNode, + List<? extends INode> childNodes) { + super.paintSelectionFeedback(graphics, parentNode, childNodes); + + boolean showDependents = true; + if (RelativeLayoutRule.sShowStructure) { + childNodes = Arrays.asList(parentNode.getChildren()); + // Avoid painting twice - both as incoming and outgoing + showDependents = false; + } else if (!RelativeLayoutRule.sShowConstraints) { + return; } - Rect bn = targetNode.getBounds(); - if (!bn.isValid()) { - return null; - } + ConstraintPainter.paintSelectionFeedback(graphics, parentNode, childNodes, showDependents); + } - // Collect the ids of the elements being dragged - List<String> movedIds = new ArrayList<String>(collectIds( - new HashMap<String, Pair<String, String>>(), elements).keySet()); + // ==== Drag'n'drop support ==== - // Prepare the drop feedback - return new DropFeedback(new RelativeDropData(movedIds), new IFeedbackPainter() { - public void paint(IGraphics gc, INode node, DropFeedback feedback) { - drawRelativeDropFeedback(gc, node, elements, feedback); - } - }); + @Override + public DropFeedback onDropEnter(INode targetNode, final IDragElement[] elements) { + return new DropFeedback(new MoveHandler(targetNode, elements, mRulesEngine), + new GuidelinePainter()); } @Override public DropFeedback onDropMove(INode targetNode, IDragElement[] elements, DropFeedback feedback, Point p) { - - RelativeDropData data = (RelativeDropData) feedback.userData; - Rect area = feedback.captureArea; - - // Only look for a new child if cursor is no longer under the current - // rect - if (area == null || !area.contains(p)) { - - // We're not capturing anymore since we got outside of the capture - // bounds - feedback.captureArea = null; - feedback.requestPaint = false; - data.setRejected(null); - - // Find the current direct children under the cursor - INode childNode = null; - int childIndex = -1; - nextChild: for (INode child : targetNode.getChildren()) { - childIndex++; - Rect bc = child.getBounds(); - if (bc.contains(p)) { - - // If we're doing a move operation within the same canvas, - // we can't attach the moved object to one belonging to the - // selection since it will disappear after the move. - if (feedback.sameCanvas && !feedback.isCopy) { - for (IDragElement element : elements) { - if (bc.equals(element.getBounds())) { - data.setRejected(bc); - feedback.requestPaint = true; - continue nextChild; - } - } - } - - // One more limitation: if we're moving one or more - // elements, we can't drop them on a child which relative - // position is expressed directly or indirectly based on the - // element being moved. - if (!feedback.isCopy) { - if (searchRelativeIds(child, data.getMovedIds(), - data.getCachedLinkIds())) { - data.setRejected(bc); - feedback.requestPaint = true; - continue nextChild; - } - } - - childNode = child; - break; - } - } - - // If there is a selected child and it changed, recompute child drop - // zones - if (childNode != null && childNode != data.getChild()) { - data.setChild(childNode); - data.setIndex(childIndex); - data.setCurr(null); - data.setZones(null); - - Pair<Rect, List<DropZone>> result = computeChildDropZones(childNode); - data.setZones(result.getSecond()); - - // Capture this rect, to prevent the engine from switching the - // layout node. - feedback.captureArea = result.getFirst(); - feedback.requestPaint = true; - - } else if (childNode == null) { - // If there is no selected child, compute the border drop zone - data.setChild(null); - data.setIndex(-1); - data.setCurr(null); - - DropZone zone = computeBorderDropZone(targetNode, p, feedback); - if (zone == null) { - data.setZones(null); - } else { - data.setZones(Collections.singletonList(zone)); - feedback.captureArea = zone.getRect(); - } - - feedback.requestPaint |= (area == null || !area.equals(feedback.captureArea)); - } - } - - // Find the current zone - DropZone currZone = null; - if (data.getZones() != null) { - for (DropZone zone : data.getZones()) { - if (zone.getRect().contains(p)) { - currZone = zone; - break; - } - } - - // Look to see if there's a border match if we didn't find anything better; - // a border match isn't required to have the mouse cursor within it since we - // do edge matching in the code which -adds- the border zones. - if (currZone == null && feedback.dragBounds != null) { - for (DropZone zone : data.getZones()) { - if (zone.isBorderZone()) { - currZone = zone; - break; - } - } - } - } - - // Look for border match when there are no children: always offer one in this case - if (currZone == null && targetNode.getChildren().length == 0 && data.getZones() != null - && data.getZones().size() > 0) { - currZone = data.getZones().get(0); + if (elements == null || elements.length == 0) { + return null; } - if (currZone != data.getCurr()) { - data.setCurr(currZone); - feedback.requestPaint = true; - } + MoveHandler state = (MoveHandler) feedback.userData; + int offsetX = p.x + (feedback.dragBounds != null ? feedback.dragBounds.x : 0); + int offsetY = p.y + (feedback.dragBounds != null ? feedback.dragBounds.y : 0); + state.updateMove(feedback, elements, offsetX, offsetY, feedback.modifierMask); - feedback.invalidTarget = (currZone == null); + // Or maybe only do this if the results changed... + feedback.requestPaint = true; return feedback; } - /** - * Returns true if the child has any attribute of Format.REFERENCE which - * value matches one of the ids in movedIds. - */ - private boolean searchRelativeIds(INode node, List<String> movedIds, - Map<INode, Set<String>> cachedLinkIds) { - Set<String> ids = getLinkedIds(node, cachedLinkIds); - - for (String id : ids) { - if (movedIds.contains(id)) { - return true; - } - } - - return false; - } - - private Set<String> getLinkedIds(INode node, Map<INode, Set<String>> cachedLinkIds) { - Set<String> ids = cachedLinkIds.get(node); - - if (ids != null) { - return ids; - } - - // We don't have cached data on this child, so create a list of - // all the linked id it is referencing. - ids = new HashSet<String>(); - cachedLinkIds.put(node, ids); - for (IAttribute attr : node.getLiveAttributes()) { - IAttributeInfo attrInfo = node.getAttributeInfo(attr.getUri(), attr.getName()); - if (attrInfo == null) { - continue; - } - Format[] formats = attrInfo.getFormats(); - if (!IAttributeInfo.Format.REFERENCE.in(formats)) { - continue; - } - - String id = attr.getValue(); - id = normalizeId(id); - if (ids.contains(id)) { - continue; - } - ids.add(id); - - // Find the sibling with that id - INode p = node.getParent(); - if (p == null) { - continue; - } - for (INode child : p.getChildren()) { - if (child == node) { - continue; - } - String childId = child.getStringAttr(ANDROID_URI, ATTR_ID); - if (childId == null) { - continue; - } - childId = normalizeId(childId); - if (id.equals(childId)) { - Set<String> linkedIds = getLinkedIds(child, cachedLinkIds); - ids.addAll(linkedIds); - break; - } - } - } - - return ids; - } - - private DropZone computeBorderDropZone(INode targetNode, Point p, DropFeedback feedback) { - Rect bounds = targetNode.getBounds(); - int x = p.x; - int y = p.y; - - int x1 = bounds.x; - int y1 = bounds.y; - int w = bounds.w; - int h = bounds.h; - int x2 = x1 + w; - int y2 = y1 + h; - - // Default border zone size - int n = 10; - int n2 = 2*n; - - // Size of -matched- border zone (not painted, but we detect edge overlaps here) - int hn = 0; - int vn = 0; - if (feedback.dragBounds != null) { - hn = feedback.dragBounds.w / 2; - vn = feedback.dragBounds.h / 2; - } - boolean vertical = false; - - Rect r = null; - String attr = null; - - if (x <= x1 + n + hn && y >= y1 && y <= y2) { - r = new Rect(x1 - n, y1, n2, h); - attr = VALUE_ALIGN_PARENT_LEFT; - vertical = true; - - } else if (x >= x2 - hn - n && y >= y1 && y <= y2) { - r = new Rect(x2 - n, y1, n2, h); - attr = VALUE_ALIGN_PARENT_RIGHT; - vertical = true; - - } else if (y <= y1 + n + vn && x >= x1 && x <= x2) { - r = new Rect(x1, y1 - n, w, n2); - attr = VALUE_ALIGN_PARENT_TOP; - - } else if (y >= y2 - vn - n && x >= x1 && x <= x2) { - r = new Rect(x1, y2 - n, w, n2); - attr = VALUE_ALIGN_PARENT_BOTTOM; - - } else { - // We're nowhere near a border. - // If there are no children, we will offer one anyway: - if (targetNode.getChildren().length == 0) { - r = new Rect(x1 - n, y1, n2, h); - attr = VALUE_ALIGN_PARENT_LEFT; - vertical = true; - } else { - return null; - } - } - - return new DropZone(r, Collections.singletonList(attr), r.getCenter(), vertical); - } - - private Pair<Rect, List<DropZone>> computeChildDropZones(INode childNode) { - - Rect b = childNode.getBounds(); - - // Compute drop zone borders as follow: - // - // +---+-----+-----+-----+---+ - // | 1 \ 2 \ 3 / 4 / 5 | - // +----+-----+---+-----+----+ - // - // For the top and bottom borders, zones 1 and 5 have the same width, - // which is - // size1 = min(10, w/5) - // and zones 2, 3 and 4 have a width of - // size2 = (w - 2*size) / 3 - // - // Same works for left and right borders vertically. - // - // Attributes generated: - // Horizontally: - // 1- toLeftOf / 2- alignLeft / 3- 2+4 / 4- alignRight / 5- toRightOf - // Vertically: - // 1- above / 2-alignTop / 3- 2+4 / 4- alignBottom / 5- below - - int w1 = 20; - int w3 = b.w / 3; - int w2 = Math.max(20, w3); - - int h1 = 20; - int h3 = b.h / 3; - int h2 = Math.max(20, h3); - - int wt = w1 * 2 + w2 * 3; - int ht = h1 * 2 + h2 * 3; - - int x1 = b.x + ((b.w - wt) / 2); - int y1 = b.y + ((b.h - ht) / 2); - - Rect bounds = new Rect(x1, y1, wt, ht); - - List<DropZone> zones = new ArrayList<DropZone>(16); - String a = VALUE_ABOVE; - int x = x1; - int y = y1; - - x = addx(w1, a, x, y, h1, zones, VALUE_TO_LEFT_OF); - x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_LEFT); - x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_LEFT, VALUE_ALIGN_RIGHT); - x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_RIGHT); - x = addx(w1, a, x, y, h1, zones, VAUE_TO_RIGHT_OF); - - a = VALUE_BELOW; - x = x1; - y = y1 + ht - h1; - - x = addx(w1, a, x, y, h1, zones, VALUE_TO_LEFT_OF); - x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_LEFT); - x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_LEFT, VALUE_ALIGN_RIGHT); - x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_RIGHT); - x = addx(w1, a, x, y, h1, zones, VAUE_TO_RIGHT_OF); - - a = VALUE_TO_LEFT_OF; - x = x1; - y = y1 + h1; - - y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_TOP); - y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_TOP, VALUE_ALIGN_BOTTOM); - y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_BOTTOM); - - a = VAUE_TO_RIGHT_OF; - x = x1 + wt - w1; - y = y1 + h1; - - y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_TOP); - y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_TOP, VALUE_ALIGN_BOTTOM); - y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_BOTTOM); - - return Pair.of(bounds, zones); - } - - private int addx(int wn, String a, int x, int y, int h1, List<DropZone> zones, String... a2) { - Rect rect = new Rect(x, y, wn, h1); - List<String> attrs = new ArrayList<String>(a2.length + 1); - attrs.add(a); - for (String attribute : a2) { - attrs.add(attribute); - } - zones.add(new DropZone(rect, attrs)); - return x + wn; - } - - private int addy(int hn, String a, int x, int y, int w1, List<DropZone> zones, String... a2) { - Rect rect = new Rect(x, y, w1, hn); - List<String> attrs = new ArrayList<String>(a2.length + 1); - attrs.add(a); - for (String attribute : a2) { - attrs.add(attribute); - } - - zones.add(new DropZone(rect, attrs)); - return y + hn; - } - - private void drawRelativeDropFeedback(IGraphics gc, INode targetNode, IDragElement[] elements, - DropFeedback feedback) { - Rect b = targetNode.getBounds(); - if (!b.isValid()) { - return; - } - - gc.useStyle(DrawingStyle.DROP_RECIPIENT); - gc.drawRect(b); - - gc.useStyle(DrawingStyle.DROP_ZONE); - - RelativeDropData data = (RelativeDropData) feedback.userData; - - if (data.getZones() != null) { - for (DropZone it : data.getZones()) { - gc.drawRect(it.getRect()); - } - } - - if (data.getCurr() != null) { - gc.useStyle(DrawingStyle.DROP_ZONE_ACTIVE); - gc.fillRect(data.getCurr().getRect()); - - Rect r = feedback.captureArea; - int x = r.x + 5; - int y = r.y + r.h + 5; - - String id = null; - if (data.getChild() != null) { - id = data.getChild().getStringAttr(ANDROID_URI, ATTR_ID); - } - - // Print constraints (with id appended if applicable) - gc.useStyle(DrawingStyle.HELP); - List<String> strings = new ArrayList<String>(); - for (String it : data.getCurr().getAttr()) { - strings.add(id != null && id.length() > 0 ? it + "=" + id : it); - } - gc.drawBoxedStrings(x, y, strings); - - Point mark = data.getCurr().getMark(); - if (mark != null) { - gc.useStyle(DrawingStyle.DROP_PREVIEW); - Rect nr = data.getCurr().getRect(); - int nx = nr.x + nr.w / 2; - int ny = nr.y + nr.h / 2; - boolean vertical = data.getCurr().isVertical(); - if (vertical) { - gc.drawLine(nx, nr.y, nx, nr.y + nr.h); - x = nx; - y = b.y; - } else { - gc.drawLine(nr.x, ny, nr.x + nr.w, ny); - x = b.x; - y = ny; - } - } else { - r = data.getCurr().getRect(); - x = r.x + r.w / 2; - y = r.y + r.h / 2; - } - - Rect be = elements[0].getBounds(); - - // Draw bound rectangles for all selected items - gc.useStyle(DrawingStyle.DROP_PREVIEW); - for (IDragElement element : elements) { - be = element.getBounds(); - if (!be.isValid()) { - // We don't always have bounds - for example when dragging - // from the palette. - continue; - } - - int offsetX = x - be.x; - int offsetY = y - be.y; - - if (data.getCurr().getAttr().contains(VALUE_ALIGN_TOP) - && data.getCurr().getAttr().contains(VALUE_ALIGN_BOTTOM)) { - offsetY -= be.h / 2; - } else if (data.getCurr().getAttr().contains(VALUE_ABOVE) - || data.getCurr().getAttr().contains(VALUE_ALIGN_TOP) - || data.getCurr().getAttr().contains(VALUE_ALIGN_PARENT_BOTTOM)) { - offsetY -= be.h; - } - if (data.getCurr().getAttr().contains(VALUE_ALIGN_RIGHT) - && data.getCurr().getAttr().contains(VALUE_ALIGN_LEFT)) { - offsetX -= be.w / 2; - } else if (data.getCurr().getAttr().contains(VALUE_TO_LEFT_OF) - || data.getCurr().getAttr().contains(VALUE_ALIGN_LEFT) - || data.getCurr().getAttr().contains(VALUE_ALIGN_PARENT_RIGHT)) { - offsetX -= be.w; - } - - drawElement(gc, element, offsetX, offsetY); - } - } - - if (data.getRejected() != null) { - Rect br = data.getRejected(); - gc.useStyle(DrawingStyle.INVALID); - gc.fillRect(br); - gc.drawLine(br.x, br.y, br.x + br.w, br.y + br.h); - gc.drawLine(br.x, br.y + br.h, br.x + br.w, br.y); - } - } - @Override public void onDropLeave(INode targetNode, IDragElement[] elements, DropFeedback feedback) { - // Free the last captured rect, if any - feedback.captureArea = null; } @Override public void onDropped(final INode targetNode, final IDragElement[] elements, final DropFeedback feedback, final Point p) { - final RelativeDropData data = (RelativeDropData) feedback.userData; - if (data.getCurr() == null) { - return; - } + final MoveHandler state = (MoveHandler) feedback.userData; - // Collect IDs from dropped elements and remap them to new IDs - // if this is a copy or from a different canvas. final Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements, feedback.isCopy || !feedback.sameCanvas); - targetNode.editXml("Add elements to RelativeLayout", new INodeHandler() { + targetNode.editXml("Dropped", new INodeHandler() { + public void handle(INode n) { + int index = -1; - public void handle(INode node) { - int index = data.getIndex(); + // Remove cycles + state.removeCycles(); // Now write the new elements. for (IDragElement element : elements) { @@ -636,25 +209,58 @@ public class RelativeLayoutRule extends BaseLayoutRule { INode newChild = targetNode.insertChildAt(fqcn, index); // Copy all the attributes, modifying them as needed. - addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER); + addAttributes(newChild, element, idMap, BaseLayoutRule.DEFAULT_ATTR_FILTER); + addInnerElements(newChild, element, idMap); - // TODO... seems totally wrong. REVISIT or EXPLAIN - String id = null; - if (data.getChild() != null) { - id = data.getChild().getStringAttr(ANDROID_URI, ATTR_ID); - } + state.applyConstraints(newChild); + } + } + }); + } - for (String it : data.getCurr().getAttr()) { - newChild.setAttribute(ANDROID_URI, - ATTR_LAYOUT_PREFIX + it, id != null ? id : "true"); //$NON-NLS-1$ - } + @Override + public void onChildInserted(INode node, INode parent, InsertType insertType) { + // TODO: Handle more generically some way to ensure that widgets with no + // intrinsic size get some minimum size until they are attached on multiple + // opposing sides. + //String fqcn = node.getFqcn(); + //if (fqcn.equals(FQCN_EDIT_TEXT)) { + // node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, "100dp"); //$NON-NLS-1$ + //} + } - addInnerElements(newChild, element, idMap); - } + // ==== Resize Support ==== + + @Override + public DropFeedback onResizeBegin(INode child, INode parent, + SegmentType horizontalEdgeType, SegmentType verticalEdgeType) { + ResizeHandler state = new ResizeHandler(parent, child, mRulesEngine, + horizontalEdgeType, verticalEdgeType); + return new DropFeedback(state, new GuidelinePainter()); + } + + @Override + public void onResizeUpdate(DropFeedback feedback, INode child, INode parent, Rect newBounds, + int modifierMask) { + ResizeHandler state = (ResizeHandler) feedback.userData; + state.updateResize(feedback, child, newBounds, modifierMask); + } + + @Override + public void onResizeEnd(DropFeedback feedback, INode child, INode parent, + final Rect newBounds) { + final ResizeHandler state = (ResizeHandler) feedback.userData; + + child.editXml("Resize", new INodeHandler() { + public void handle(INode n) { + state.removeCycles(); + state.applyConstraints(n); } }); } + // ==== Layout Actions Bar ==== + @Override public void addLayoutActions(List<MenuAction> actions, final INode parentNode, final List<? extends INode> children) { @@ -664,133 +270,95 @@ public class RelativeLayoutRule extends BaseLayoutRule { ATTR_GRAVITY)); actions.add(MenuAction.createSeparator(25)); actions.add(createMarginAction(parentNode, children)); - } - - /** - * Internal state used by the RelativeLayoutRule, stored as userData in the - * {@link DropFeedback}. - */ - private static class RelativeDropData { - /** Current child under cursor */ - private INode mChild; - - /** Index of child in the parent children list */ - private int mIndex; - - /** - * Valid "anchor" zones for the current child of type - */ - private List<DropZone> mZones; - - /** Current zone */ - private DropZone mCurr; - - /** rejected target (Rect bounds) */ - private Rect mRejected; - private List<String> mMovedIds; - - private Map<INode, Set<String>> mCachedLinkIds = new HashMap<INode, Set<String>>(); - - public RelativeDropData(List<String> movedIds) { - this.mMovedIds = movedIds; - } - - private void setChild(INode child) { - this.mChild = child; - } - - private INode getChild() { - return mChild; - } - - private void setIndex(int index) { - this.mIndex = index; - } - - private int getIndex() { - return mIndex; - } - - private void setZones(List<DropZone> zones) { - this.mZones = zones; - } - - private List<DropZone> getZones() { - return mZones; - } - - private void setCurr(DropZone curr) { - this.mCurr = curr; - } - - private DropZone getCurr() { - return mCurr; - } - - private void setRejected(Rect rejected) { - this.mRejected = rejected; - } - - private Rect getRejected() { - return mRejected; - } - - private List<String> getMovedIds() { - return mMovedIds; - } + IMenuCallback callback = new IMenuCallback() { + public void action(MenuAction action, final String valueId, final Boolean newValue) { + final String id = action.getId(); + if (id.equals(ACTION_CENTER_VERTICAL)|| id.equals(ACTION_CENTER_HORIZONTAL)) { + parentNode.editXml("Center", new INodeHandler() { + public void handle(INode n) { + if (id.equals(ACTION_CENTER_VERTICAL)) { + for (INode child : children) { + centerVertically(child); + } + } else if (id.equals(ACTION_CENTER_HORIZONTAL)) { + for (INode child : children) { + centerHorizontally(child); + } + } + mRulesEngine.redraw(); + } - private Map<INode, Set<String>> getCachedLinkIds() { - return mCachedLinkIds; - } + }); + } else if (id.equals(ACTION_SHOW_CONSTRAINTS)) { + sShowConstraints = !sShowConstraints; + mRulesEngine.redraw(); + } else { + assert id.equals(ACTION_SHOW_STRUCTURE); + sShowStructure = !sShowStructure; + mRulesEngine.redraw(); + } + } + }; + + // Centering actions + if (children != null && children.size() > 0) { + actions.add(MenuAction.createSeparator(150)); + actions.add(MenuAction.createAction(ACTION_CENTER_VERTICAL, "Center Vertically", null, + callback, ICON_CENTER_VERTICALLY, 160)); + actions.add(MenuAction.createAction(ACTION_CENTER_HORIZONTAL, "Center Horizontally", + null, callback, ICON_CENTER_HORIZONTALLY, 170)); + } + + actions.add(MenuAction.createSeparator(80)); + actions.add(MenuAction.createToggle(ACTION_SHOW_CONSTRAINTS, "Show Constraints", + sShowConstraints, callback, ICON_SHOW_CONSTRAINTS, 180)); + actions.add(MenuAction.createToggle(ACTION_SHOW_STRUCTURE, "Show All Relationships", + sShowStructure, callback, ICON_SHOW_STRUCTURE, 190)); } - private static class DropZone { - /** The rectangular bounds of the drop zone */ - private final Rect mRect; - - /** - * Attributes that correspond to this drop zone, e.g. ["alignLeft", - * "alignBottom"] - */ - private final List<String> mAttr; - - /** Non-null iff this is a border */ - private final Point mMark; - - /** Defined iff this is a border match */ - private final boolean mVertical; - - public DropZone(Rect rect, List<String> attr, Point mark, boolean vertical) { - super(); - this.mRect = rect; - this.mAttr = attr; - this.mMark = mark; - this.mVertical = vertical; - } - - public DropZone(Rect rect, List<String> attr) { - this(rect, attr, null, false); - } - - private Rect getRect() { - return mRect; - } - - private List<String> getAttr() { - return mAttr; - } - - private Point getMark() { - return mMark; - } - - private boolean isVertical() { - return mVertical; + private void centerHorizontally(INode node) { + // Clear horizontal-oriented attributes from the node + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_LEFT, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_LEFT, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_RIGHT, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_RIGHT, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null); + + if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT))) { + // Already done + } else if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, + ATTR_LAYOUT_CENTER_VERTICAL))) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, VALUE_TRUE); + } else { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, VALUE_TRUE); } + } - private boolean isBorderZone() { - return mMark != null; + private void centerVertically(INode node) { + // Clear vertical-oriented attributes from the node + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_TOP, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_TOP, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_BELOW, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BOTTOM, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ABOVE, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null); + + // Center vertically + if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT))) { + // ALready done + } else if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, + ATTR_LAYOUT_CENTER_HORIZONTAL))) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, VALUE_TRUE); + } else { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, VALUE_TRUE); } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableLayoutRule.java index cc67d3a..d556e7d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableLayoutRule.java @@ -17,6 +17,7 @@ package com.android.ide.common.layout; import static com.android.ide.common.layout.LayoutConstants.FQCN_TABLE_ROW; +import com.android.ide.common.api.DropFeedback; import com.android.ide.common.api.IClientRulesEngine; import com.android.ide.common.api.IMenuCallback; import com.android.ide.common.api.INode; @@ -24,6 +25,7 @@ import com.android.ide.common.api.INodeHandler; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.InsertType; import com.android.ide.common.api.MenuAction; +import com.android.ide.common.api.SegmentType; import java.net.URL; import java.util.Collections; @@ -175,4 +177,23 @@ public class TableLayoutRule extends LinearLayoutRule { } } } + + @Override + public DropFeedback onResizeBegin(INode child, INode parent, SegmentType horizontalEdge, + SegmentType verticalEdge) { + // Children of a table layout cannot set their widths (it is controlled by column + // settings on the table). They can set their heights (though for TableRow, the + // height is always wrap_content). + if (horizontalEdge == null) { // Widths are edited by vertical edges. + // The user is not editing a vertical height so don't allow resizing at all + return null; + } + if (child.getFqcn().equals(FQCN_TABLE_ROW)) { + // TableRows are always WRAP_CONTENT + return null; + } + + // Allow resizing heights only + return super.onResizeBegin(child, parent, horizontalEdge, null /*verticalEdge*/); + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableRowRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableRowRule.java index ac03653..031e17b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableRowRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableRowRule.java @@ -17,10 +17,12 @@ package com.android.ide.common.layout; import static com.android.ide.common.layout.LayoutConstants.FQCN_TABLE_LAYOUT; +import com.android.ide.common.api.DropFeedback; import com.android.ide.common.api.INode; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.InsertType; import com.android.ide.common.api.MenuAction; +import com.android.ide.common.api.SegmentType; import java.util.List; @@ -61,4 +63,12 @@ public class TableRowRule extends LinearLayoutRule { } } } + + @Override + public DropFeedback onResizeBegin(INode child, INode parent, SegmentType horizontalEdge, + SegmentType verticalEdge) { + // No resizing in TableRows; the width is *always* match_parent and the height is + // *always* wrap_content. + return null; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/centerHorizontally.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/centerHorizontally.png Binary files differnew file mode 100644 index 0000000..5053cda --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/centerHorizontally.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/centerVertically.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/centerVertically.png Binary files differnew file mode 100644 index 0000000..ebba8e8 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/centerVertically.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/constraints.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/constraints.png Binary files differnew file mode 100644 index 0000000..7247d5a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/constraints.png 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 0000000..447d2d8 --- /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 0000000..8488760 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ConstraintType.java @@ -0,0 +1,236 @@ +/* + * 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.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ABOVE; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BOTTOM; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_LEFT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_RIGHT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_TOP; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_IN_PARENT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF; + +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} + */ + public static ConstraintType fromAttribute(String attribute) { + if (sNameToType == null) { + ConstraintType[] types = ConstraintType.values(); + sNameToType = new HashMap<String, ConstraintType>(types.length); + for (ConstraintType type : types) { + sNameToType.put(type.name, type); + } + } + 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 + */ + 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; + } +}
\ No newline at end of file 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 0000000..a0039fb --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java @@ -0,0 +1,322 @@ +/* + * 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.layout.LayoutConstants.ANDROID_URI; +import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX; +import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE; + +import com.android.ide.common.api.IDragElement; +import com.android.ide.common.api.INode; +import com.android.ide.common.api.IDragElement.IDragAttribute; +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 dependencies, otherwise look for + * horizontal 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) { + 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_PREFIX)) { + return name.substring(ATTR_LAYOUT_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 0000000..8faf364 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java @@ -0,0 +1,765 @@ +/* + * 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.layout.BaseLayoutRule.getMaxMatchDistance; +import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ABOVE; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BOTTOM; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_LEFT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_RIGHT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_TOP; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_IN_PARENT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_BOTTOM; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_LEFT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_RIGHT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_TOP; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF; +import static com.android.ide.common.layout.LayoutConstants.VALUE_N_DP; +import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE; +import static com.android.ide.common.layout.relative.ConstraintType.ALIGN_BASELINE; +import static com.android.ide.common.layout.relative.MarginType.NO_MARGIN; +import static com.android.ide.common.layout.relative.MarginType.WITHOUT_MARGIN; +import static com.android.ide.common.layout.relative.MarginType.WITH_MARGIN; +import static java.lang.Math.abs; + +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. + */ + protected Set<INode> mHorizontalDeps; + + /** + * The set of nodes which depend on the currently selected nodes, including + * transitively, through vertical constraints. + */ + 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; + } + + 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(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 + */ + 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()); + if (mCurrentTopMatch.type == ALIGN_BASELINE) { + // HACK! WORKAROUND! Baseline doesn't provide a new bottom edge for attachments + String c = mCurrentTopMatch.getConstraint(); + c = c.replace(ATTR_LAYOUT_ALIGN_BASELINE, ATTR_LAYOUT_ALIGN_BOTTOM); + applyConstraint(n, c); + } + } + + if (mMoveBottom && mCurrentBottomMatch != null) { + applyConstraint(n, mCurrentBottomMatch.getConstraint()); + } + + if (mMoveLeft && mCurrentLeftMatch != null) { + applyConstraint(n, mCurrentLeftMatch.getConstraint()); + } + + if (mMoveRight && mCurrentRightMatch != null) { + applyConstraint(n, mCurrentRightMatch.getConstraint()); + } + + 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); + } + } + + 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> { + 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; + } + } +}
\ No newline at end of file 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 0000000..158a792 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelinePainter.java @@ -0,0 +1,186 @@ +/* + * 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.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_BOTTOM; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_LEFT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_RIGHT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_TOP; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX; +import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX; +import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; + +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.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 ---- + public void paint(IGraphics gc, INode node, 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); + } + + 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) { + gc.useStyle(DrawingStyle.HELP); + Rect b = state.layout.getBounds(); + int x, y; + if (b.w > b.h) { + x = b.x + 3; + y = b.y2() + 6; + } else { + x = b.x2() + 6; + y = b.y + 3; + } + + gc.drawBoxedStrings(x, y, strings); + } + + 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(); + String description = constraint.replace(NEW_ID_PREFIX, "").replace(ID_PREFIX, ""); + if (description.startsWith(ATTR_LAYOUT_PREFIX)) { + description = description.substring(ATTR_LAYOUT_PREFIX.length()); + } + if (margin > 0) { + description = String.format("%1$s, margin=%2$d dp", description, margin); + } + 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)); + } + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/MarginType.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/MarginType.java new file mode 100644 index 0000000..7705958 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/MarginType.java @@ -0,0 +1,52 @@ +/* + * 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 com.android.ide.common.api.Segment; + +/** + * A {@link MarginType} indicates whether a {@link Segment} corresponds to the visual edge + * of the node, or whether it is offset by a margin in the edge's direction, or whether + * it's both (which is the case when the margin is 0). + * <p> + * We need to keep track of the distinction because different constraints apply + * differently w.r.t. margins. Let's say you have a target node with a 50 dp margin in all + * directions. If you layout_alignTop with this node, the match will be on the visual + * bounds of the target node (ignoring the margin). If you layout_above this node, you + * will be offset by the margin on the target node. Therefore, we have to add <b>both</b> + * edges (the bounds of the target node with and without edges) and check for matches on + * each edge depending on the constraint being considered. + */ +public enum MarginType { + /** + * This margin type is used for nodes that have margins, and this segment includes the + * margin distance + */ + WITH_MARGIN, + + /** + * This margin type is used for nodes that have margins, and this segment does not + * include the margin distance + */ + WITHOUT_MARGIN, + + /** + * This margin type is used for nodes that do not have margins, so margin edges and + * non-margin edges are the same + */ + NO_MARGIN; +} 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 0000000..a341113 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/Match.java @@ -0,0 +1,73 @@ +/* + * 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.layout.LayoutConstants.VALUE_TRUE; + +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; + + /** + * Create a new 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(Segment edge, Segment with, ConstraintType type, int delta) { + this.edge = edge; + this.with = with; + this.type = type; + this.delta = delta; + } + + /** + * Returns the XML constraint attribute value for this match + * + * @return the XML constraint attribute value for this match + */ + public String getConstraint() { + if (type.targetParent) { + return type.name + '=' + VALUE_TRUE; + } else { + String id = edge.id; + return type.name + '=' + id; + } + } + + @Override + public String toString() { + return "Match [type=" + type + ", delta=" + delta + ", edge=" + edge + + "]"; + } +}
\ No newline at end of file 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 0000000..c7d25b4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/MoveHandler.java @@ -0,0 +1,284 @@ +/* + * 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.layout.LayoutConstants.ANDROID_URI; +import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; +import static com.android.ide.common.layout.relative.MarginType.NO_MARGIN; +import static java.lang.Math.abs; + +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 { + public int mDraggedBaseline; + + 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 /* vertical */); + mVerticalDeps = mDependencyGraph.dependsOn(nodes, true /* vertical */); + + 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) { + // Need an id to reference child in attachments to it, so skip + // nodes without ids + String id = child.getStringAttr(ANDROID_URI, ATTR_ID); + if (id == null) { + continue; + } + + 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; + } + } + + 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 (mVerticalDeps.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 (mHorizontalDeps.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); + } +}
\ No newline at end of file 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 0000000..870798d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ResizeHandler.java @@ -0,0 +1,255 @@ +/* + * 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.layout.LayoutConstants.ANDROID_URI; +import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; +import static com.android.ide.common.layout.relative.MarginType.NO_MARGIN; +import static java.lang.Math.abs; + +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 { + public final INode mResized; + public final SegmentType mHorizontalEdgeType; + public final SegmentType mVerticalEdgeType; + + 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; + + mResized = resized; + 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) { + // Need an id to reference child in attachments to it, so skip nodes + // without ids + // TODO: Generate an id on the fly when needed (at commit time) instead! + String id = child.getStringAttr(ANDROID_URI, ATTR_ID); + if (id == null) { + continue; + } + + 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; + } + + 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 (mVerticalDeps.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 (mHorizontalDeps.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); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/structure.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/structure.png Binary files differnew file mode 100644 index 0000000..e5d7538 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/structure.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java index ca06477..7da9587 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java @@ -57,7 +57,7 @@ import java.util.regex.Pattern; * <p/> * This pull parser generates {@link ViewInfo}s which key is a {@link UiElementNode}. */ -public final class UiElementPullParser extends BasePullParser { +public class UiElementPullParser extends BasePullParser { private final static String ATTR_PADDING = "padding"; //$NON-NLS-1$ private final static Pattern FLOAT_PATTERN = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)"); //$NON-NLS-1$ @@ -104,7 +104,7 @@ public final class UiElementPullParser extends BasePullParser { * (without padding) would be invisible. This parameter can be null, in which case * nodes are not individually exploded (but they may all be exploded with the * explodeRendering parameter. - * @param densityValue the density factor for the screen. + * @param density the density factor for the screen. * @param xdpi the screen actual dpi in X * @param project Project containing this layout. */ @@ -127,7 +127,7 @@ public final class UiElementPullParser extends BasePullParser { push(mRoot); } - private UiElementNode getCurrentNode() { + protected UiElementNode getCurrentNode() { if (mNodeStack.size() > 0) { return mNodeStack.get(mNodeStack.size()-1); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java index 286591f..6bd03b2 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java @@ -19,6 +19,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import static com.android.ide.common.layout.LayoutConstants.GESTURE_OVERLAY_VIEW; import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_MERGE; +import com.android.ide.common.api.Margins; import com.android.ide.common.api.Rect; import com.android.ide.common.rendering.api.Capability; import com.android.ide.common.rendering.api.MergeCookie; @@ -75,6 +76,7 @@ public class CanvasViewInfo implements IPropertySource { private final Object mViewObject; private final UiViewElementNode mUiViewNode; private CanvasViewInfo mParent; + private ViewInfo mViewInfo; private final ArrayList<CanvasViewInfo> mChildren = new ArrayList<CanvasViewInfo>(); /** @@ -100,10 +102,11 @@ public class CanvasViewInfo implements IPropertySource { */ private CanvasViewInfo(CanvasViewInfo parent, String name, Object viewObject, UiViewElementNode node, Rectangle absRect, - Rectangle selectionRect) { + Rectangle selectionRect, ViewInfo viewInfo) { mParent = parent; mName = name; mViewObject = viewObject; + mViewInfo = viewInfo; mUiViewNode = node; mAbsRect = absRect; mSelectionRect = selectionRect; @@ -274,6 +277,39 @@ public class CanvasViewInfo implements IPropertySource { return mViewObject; } + public int getBaseline() { + if (mViewInfo != null) { + int baseline = mViewInfo.getBaseLine(); + if (baseline != Integer.MIN_VALUE) { + return baseline; + } + } + + return -1; + } + + /** + * Returns the {@link Margins} for this {@link CanvasViewInfo} + * + * @return the {@link Margins} for this {@link CanvasViewInfo} + */ + public Margins getMargins() { + if (mViewInfo != null) { + int leftMargin = mViewInfo.getLeftMargin(); + int topMargin = mViewInfo.getTopMargin(); + int rightMargin = mViewInfo.getRightMargin(); + int bottomMargin = mViewInfo.getBottomMargin(); + return new Margins( + leftMargin != Integer.MIN_VALUE ? leftMargin : 0, + rightMargin != Integer.MIN_VALUE ? rightMargin : 0, + topMargin != Integer.MIN_VALUE ? topMargin : 0, + bottomMargin != Integer.MIN_VALUE ? bottomMargin : 0 + ); + } + + return null; + } + // ---- Implementation of IPropertySource public Object getEditableValue() { @@ -624,7 +660,7 @@ public class CanvasViewInfo implements IPropertySource { } CanvasViewInfo mergeView = new CanvasViewInfo(rootView, VIEW_MERGE, null, - merge, absRect, absRect); + merge, absRect, absRect, null /* viewInfo */); for (CanvasViewInfo view : merged) { if (rootView.removeChild(view)) { mergeView.addChild(view); @@ -646,7 +682,8 @@ public class CanvasViewInfo implements IPropertySource { if (rootView != null && hasMergeParent(rootView.getUiViewNode())) { CanvasViewInfo merge = new CanvasViewInfo(null, VIEW_MERGE, null, (UiViewElementNode) rootView.getUiViewNode().getUiParent(), - rootView.getAbsRect(), rootView.getSelectionRect()); + rootView.getAbsRect(), rootView.getSelectionRect(), + null /* viewInfo */); // Insert the <merge> as the new real root rootView.mParent = merge; merge.addChild(rootView); @@ -732,7 +769,7 @@ public class CanvasViewInfo implements IPropertySource { Rectangle selectionRect = new Rectangle(x, y, w - 1, h - 1); return new CanvasViewInfo(parent, root.getClassName(), root.getViewObject(), node, - absRect, selectionRect); + absRect, selectionRect, root); } /** Create a subtree recursively until you run out of keys */ @@ -965,7 +1002,7 @@ public class CanvasViewInfo implements IPropertySource { Rectangle absRect = new Rectangle(parentX, parentY, 0, 0); String name = found.getDescriptor().getXmlLocalName(); CanvasViewInfo v = new CanvasViewInfo(parentView, name, null, found, - absRect, absRect); + absRect, absRect, null /* viewInfo */); // Find corresponding index in the parent view List<CanvasViewInfo> siblings = parentView.getChildren(); int insertPosition = siblings.size(); @@ -990,7 +1027,7 @@ public class CanvasViewInfo implements IPropertySource { Rectangle absRect = new Rectangle(parentX, parentY, 0, 0); String name = node.getDescriptor().getXmlLocalName(); CanvasViewInfo v = new CanvasViewInfo(parentView, name, null, node, absRect, - absRect); + absRect, null /* viewInfo */); parentView.addChild(v); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java index a117ea9..fd4f47e 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java @@ -487,4 +487,123 @@ public class GCWrapper implements IGraphics { return color; } + + // arrows + + private static final int MIN_LENGTH = 10; + + + public void drawArrow(int x1, int y1, int x2, int y2, int size) { + int arrowWidth = size; + int arrowHeight = size; + + checkGC(); + useStrokeAlpha(); + x1 = mHScale.translate(x1); + y1 = mVScale.translate(y1); + x2 = mHScale.translate(x2); + y2 = mVScale.translate(y2); + GC graphics = getGc(); + + // Make size adjustments to ensure that the arrow has enough width to be visible + if (x1 == x2 && Math.abs(y1 - y2) < MIN_LENGTH) { + int delta = (MIN_LENGTH - Math.abs(y1 - y2)) / 2; + if (y1 < y2) { + y1 -= delta; + y2 += delta; + } else { + y1 += delta; + y2-= delta; + } + + } else if (y1 == y2 && Math.abs(x1 - x2) < MIN_LENGTH) { + int delta = (MIN_LENGTH - Math.abs(x1 - x2)) / 2; + if (x1 < x2) { + x1 -= delta; + x2 += delta; + } else { + x1 += delta; + x2-= delta; + } + } + + graphics.drawLine(x1, y1, x2, y2); + + // Arrowhead: + + if (x1 == x2) { + // Vertical + if (y2 > y1) { + graphics.drawLine(x2 - arrowWidth, y2 - arrowHeight, x2, y2); + graphics.drawLine(x2 + arrowWidth, y2 - arrowHeight, x2, y2); + } else { + graphics.drawLine(x2 - arrowWidth, y2 + arrowHeight, x2, y2); + graphics.drawLine(x2 + arrowWidth, y2 + arrowHeight, x2, y2); + } + } else if (y1 == y2) { + // Horizontal + if (x2 > x1) { + graphics.drawLine(x2 - arrowHeight, y2 - arrowWidth, x2, y2); + graphics.drawLine(x2 - arrowHeight, y2 + arrowWidth, x2, y2); + } else { + graphics.drawLine(x2 + arrowHeight, y2 - arrowWidth, x2, y2); + graphics.drawLine(x2 + arrowHeight, y2 + arrowWidth, x2, y2); + } + } else { + // Compute angle: + int dy = y2 - y1; + int dx = x2 - x1; + double angle = Math.atan2(dy, dx); + double lineLength = Math.sqrt(dy * dy + dx * dx); + + // Imagine a line of the same length as the arrow, but with angle 0. + // Its two arrow lines are at (-arrowWidth, -arrowHeight) relative + // to the endpoint (x1 + lineLength, y1) stretching up to (x2,y2). + // We compute the positions of (ax,ay) for the point above and + // below this line and paint the lines to it: + double ax = x1 + lineLength - arrowHeight; + double ay = y1 - arrowWidth; + int rx = (int) (Math.cos(angle) * (ax-x1) - Math.sin(angle) * (ay-y1) + x1); + int ry = (int) (Math.sin(angle) * (ax-x1) + Math.cos(angle) * (ay-y1) + y1); + graphics.drawLine(x2, y2, rx, ry); + + ay = y1 + arrowWidth; + rx = (int) (Math.cos(angle) * (ax-x1) - Math.sin(angle) * (ay-y1) + x1); + ry = (int) (Math.sin(angle) * (ax-x1) + Math.cos(angle) * (ay-y1) + y1); + graphics.drawLine(x2, y2, rx, ry); + } + + /* TODO: Experiment with filled arrow heads? + if (x1 == x2) { + // Vertical + if (y2 > y1) { + for (int i = 0; i < arrowWidth; i++) { + graphics.drawLine(x2 - arrowWidth + i, y2 - arrowWidth + i, + x2 + arrowWidth - i, y2 - arrowWidth + i); + } + } else { + for (int i = 0; i < arrowWidth; i++) { + graphics.drawLine(x2 - arrowWidth + i, y2 + arrowWidth - i, + x2 + arrowWidth - i, y2 + arrowWidth - i); + } + } + } else if (y1 == y2) { + // Horizontal + if (x2 > x1) { + for (int i = 0; i < arrowHeight; i++) { + graphics.drawLine(x2 - arrowHeight + i, y2 - arrowHeight + i, x2 + - arrowHeight + i, y2 + arrowHeight - i); + } + } else { + for (int i = 0; i < arrowHeight; i++) { + graphics.drawLine(x2 + arrowHeight - i, y2 - arrowHeight + i, x2 + + arrowHeight - i, y2 + arrowHeight - i); + } + } + } else { + // Arbitrary angle -- need to use trig + // TODO: Implement this + } + */ + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java index c65655a..16577c1 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java @@ -124,15 +124,19 @@ public abstract class Gesture { * user is holding the key for several seconds. * * @param event The SWT event for the key press, + * @return true if this gesture consumed the key press, otherwise return false */ - public void keyPressed(KeyEvent event) { + public boolean keyPressed(KeyEvent event) { + return false; } /** * Handles a key release during the gesture. * * @param event The SWT event for the key release, + * @return true if this gesture consumed the key press, otherwise return false */ - public void keyReleased(KeyEvent event) { + public boolean keyReleased(KeyEvent event) { + return false; } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java index afffb42..62d5dcd 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java @@ -17,7 +17,10 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import com.android.ide.common.api.DropFeedback; +import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.Rect; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; +import com.android.sdklib.SdkConstants; import com.android.util.Pair; import org.eclipse.jface.action.IStatusLineManager; @@ -307,6 +310,7 @@ public class GestureManager { mLastStateMask = 0; updateMessage(null); updateCursor(mousePos); + mCanvas.redraw(); } } @@ -387,10 +391,40 @@ public class GestureManager { // same as the one we set? mDisplayingMessage = null; status.setMessage(null); + status.setErrorMessage(null); } } /** + * Returns the current mouse position as a {@link ControlPoint} + * + * @return the current mouse position as a {@link ControlPoint} + */ + public ControlPoint getCurrentControlPoint() { + return ControlPoint.create(mCanvas, mLastMouseX, mLastMouseY); + } + + /** + * Returns the current SWT modifier key mask as an {@link IViewRule} modifier mask + * + * @return the current SWT modifier key mask as an {@link IViewRule} modifier mask + */ + public int getRuleModifierMask() { + int swtMask = mLastStateMask; + int modifierMask = 0; + if ((swtMask & SWT.MOD1) != 0) { + modifierMask |= DropFeedback.MODIFIER1; + } + if ((swtMask & SWT.MOD2) != 0) { + modifierMask |= DropFeedback.MODIFIER2; + } + if ((swtMask & SWT.MOD3) != 0) { + modifierMask |= DropFeedback.MODIFIER3; + } + return modifierMask; + } + + /** * Helper class which implements the {@link MouseMoveListener}, * {@link MouseListener} and {@link KeyListener} interfaces. */ @@ -461,19 +495,58 @@ public class GestureManager { // --- KeyListener --- public void keyPressed(KeyEvent e) { - if (e.keyCode == SWT.ESC) { - ControlPoint controlPoint = ControlPoint.create(mCanvas, - mLastMouseX, mLastMouseY); - finishGesture(controlPoint, true); - return; + mLastStateMask = e.stateMask; + // Workaround for the fact that in keyPressed the current state + // mask is not yet updated + if (e.keyCode == SWT.SHIFT) { + mLastStateMask |= SWT.MOD2; + } + if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) { + if (e.keyCode == SWT.COMMAND) { + mLastStateMask |= SWT.MOD1; + } + } else { + if (e.keyCode == SWT.CTRL) { + mLastStateMask |= SWT.MOD1; + } } + // Give gestures a first chance to see and consume the key press if (mCurrentGesture != null) { - mCurrentGesture.keyPressed(e); + // unless it's "Escape", which cancels the gesture + if (e.keyCode == SWT.ESC) { + ControlPoint controlPoint = ControlPoint.create(mCanvas, + mLastMouseX, mLastMouseY); + finishGesture(controlPoint, true); + return; + } + + if (mCurrentGesture.keyPressed(e)) { + return; + } } + + // Fall back to canvas actions for the key press + mCanvas.handleKeyPressed(e); } public void keyReleased(KeyEvent e) { + mLastStateMask = e.stateMask; + // Workaround for the fact that in keyPressed the current state + // mask is not yet updated + if (e.keyCode == SWT.SHIFT) { + mLastStateMask &= ~SWT.MOD2; + } + if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) { + if (e.keyCode == SWT.COMMAND) { + mLastStateMask &= ~SWT.MOD1; + } + } else { + if (e.keyCode == SWT.CTRL) { + mLastStateMask &= ~SWT.MOD1; + } + } + if (mCurrentGesture != null) { mCurrentGesture.keyReleased(e); } @@ -741,6 +814,12 @@ public class GestureManager { int height = (int) (scale * boundingBox.height); dragBounds = new Rect(deltaX, deltaY, width, height); dragInfo.setDragBounds(dragBounds); + + // Record the baseline such that we can perform baseline alignment + // on the node as it's dragged around + NodeProxy firstNode = + mCanvas.getNodeFactory().create(mDragSelection.get(0).getViewInfo()); + dragInfo.setDragBaseline(firstNode.getBaseline()); } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GlobalCanvasDragInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GlobalCanvasDragInfo.java index 843ed11..06986cd 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GlobalCanvasDragInfo.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GlobalCanvasDragInfo.java @@ -16,6 +16,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.Rect; @@ -46,6 +47,7 @@ final class GlobalCanvasDragInfo { private Object mSourceCanvas = null; private Runnable mRemoveSourceHandler; private Rect mDragBounds; + private int mDragBaseline = -1; /** Private constructor. Use {@link #getInstance()} to retrieve the singleton. */ private GlobalCanvasDragInfo() { @@ -149,4 +151,22 @@ final class GlobalCanvasDragInfo { public void setDragBounds(Rect dragBounds) { mDragBounds = dragBounds; } + + /** + * Returns the baseline of the drag, or -1 if not applicable + * + * @return the current SWT modifier key mask as an {@link IViewRule} modifier mask + */ + public int getDragBaseline() { + return mDragBaseline; + } + + /** + * Sets the baseline of the drag + * + * @param baseline the new baseline + */ + public void setDragBaseline(int baseline) { + mDragBaseline = baseline; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java index 18cb516..efbfe59 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java @@ -22,6 +22,9 @@ import static com.android.ide.common.layout.LayoutConstants.SCROLL_VIEW; import static com.android.ide.common.layout.LayoutConstants.STRING_PREFIX; import static com.android.ide.eclipse.adt.AdtConstants.ANDROID_PKG; +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.rendering.LayoutLibrary; import com.android.ide.common.rendering.StaticRenderSession; import com.android.ide.common.rendering.api.Capability; @@ -32,6 +35,7 @@ import com.android.ide.common.rendering.api.RenderSession; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.rendering.api.Result; import com.android.ide.common.rendering.api.SessionParams; +import com.android.ide.common.rendering.api.ViewInfo; import com.android.ide.common.rendering.api.SessionParams.RenderingMode; import com.android.ide.common.resources.ResourceFile; import com.android.ide.common.resources.ResourceRepository; @@ -55,7 +59,10 @@ import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configu import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.IConfigListener; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; import com.android.ide.eclipse.adt.internal.editors.ui.DecorComposite; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; @@ -140,6 +147,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -1481,6 +1489,13 @@ public class GraphicalEditorPart extends EditorPart mTargetSdkVersion, logger); + // Request margin and baseline information. + // TODO: Be smarter about setting this; start without it, and on the first request + // for an extended view info, re-render in the same session, and then set a flag + // which will cause this to create extended view info each time from then on in the + // same session + params.setExtendedViewInfoMode(true); + if (noDecor) { params.setForceNoDecor(); } else { @@ -1555,6 +1570,116 @@ public class GraphicalEditorPart extends EditorPart } /** + * Measure the children of the given parent node, applying the given filter to the + * pull parser's attribute values. + * + * @param parent the parent node to measure children for + * @param filter the filter to apply to the attribute values + * @return a map from node children of the parent to new bounds of the nodes + */ + public Map<INode, Rect> measureChildren(INode parent, + final IClientRulesEngine.AttributeFilter filter) { + int width = parent.getBounds().w; + int height = parent.getBounds().h; + + ResourceResolver resources = getResourceResolver(); + LayoutLibrary layoutLibrary = getLayoutLibrary(); + IProject project = getProject(); + Density density = mConfigComposite.getDensity(); + float xdpi = mConfigComposite.getXDpi(); + float ydpi = mConfigComposite.getYDpi(); + ResourceManager resManager = ResourceManager.getInstance(); + ProjectResources projectRes = resManager.getProjectResources(project); + // TODO - use mProjectCallback? If so restore logger after use + ProjectCallback projectCallback = new ProjectCallback(layoutLibrary, projectRes, project); + LayoutLog silentLogger = new LayoutLog(); + + UiElementNode parentNode = ((NodeProxy) parent).getNode(); + final NodeFactory nodeFactory = getCanvasControl().getNodeFactory(); + UiElementPullParser topParser = new UiElementPullParser(parentNode, + false, Collections.<UiElementNode>emptySet(), density, xdpi, project) { + @Override + public String getAttributeValue(String namespace, String localName) { + if (filter != null) { + Object cookie = getViewCookie(); + if (cookie instanceof UiViewElementNode) { + NodeProxy node = nodeFactory.create((UiViewElementNode) cookie); + if (node != null) { + String value = filter.getAttribute(node, namespace, localName); + if (value != null) { + return value; + } + // null means no preference, not "unset". + } + } + } + + return super.getAttributeValue(namespace, localName); + } + + /** + * The parser usually assumes that the top level node is a document node that + * should be skipped, and that's not the case when we render in the middle of + * the tree, so override {@link UiElementPullParser#onNextFromStartDocument} + * to change this behavior + */ + @Override + public void onNextFromStartDocument() { + mParsingState = START_TAG; + } + }; + + SessionParams params = new SessionParams( + topParser, + RenderingMode.FULL_EXPAND, + project /* projectKey */, + width, height, + density, xdpi, ydpi, + resources, + projectCallback, + mMinSdkVersion, + mTargetSdkVersion, + silentLogger); + params.setLayoutOnly(); + params.setForceNoDecor(); + + RenderSession session = null; + try { + projectCallback.setLogger(silentLogger); + session = layoutLibrary.createSession(params); + if (session.getResult().isSuccess()) { + assert session.getRootViews().size() == 1; + ViewInfo root = session.getRootViews().get(0); + List<ViewInfo> children = root.getChildren(); + Map<INode, Rect> map = new HashMap<INode, Rect>(children.size()); + NodeFactory factory = getCanvasControl().getNodeFactory(); + for (ViewInfo info : children) { + if (info.getCookie() instanceof UiViewElementNode) { + UiViewElementNode uiNode = (UiViewElementNode) info.getCookie(); + NodeProxy node = factory.create(uiNode); + map.put(node, new Rect(info.getLeft(), info.getTop(), + info.getRight() - info.getLeft(), + info.getBottom() - info.getTop())); + } + } + + return map; + } + } catch (RuntimeException t) { + // Exceptions from the bridge + displayError(t.getLocalizedMessage()); + throw t; + } finally { + projectCallback.setLogger(null); + if (session != null) { + session.dispose(); + } + } + + return null; + } + + /** * Returns the {@link ResourceResolver} for this editor * * @return the resolver used to resolve resources for the current configuration of diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java index 17e504a..1d36f7b 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java @@ -62,7 +62,6 @@ import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.KeyEvent; -import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MenuDetectEvent; import org.eclipse.swt.events.MenuDetectListener; import org.eclipse.swt.events.MouseEvent; @@ -144,7 +143,7 @@ public class LayoutCanvas extends Canvas { private DropTarget mDropTarget; /** Factory that can create {@link INode} proxies. */ - private final NodeFactory mNodeFactory = new NodeFactory(); + private final NodeFactory mNodeFactory = new NodeFactory(this); /** Vertical scaling & scrollbar information. */ private CanvasTransform mVScale; @@ -284,42 +283,6 @@ public class LayoutCanvas extends Canvas { } }); - addKeyListener(new KeyListener() { - - public void keyPressed(KeyEvent e) { - // Set up backspace as an alias for the delete action within the canvas. - // On most Macs there is no delete key - though there IS a key labeled - // "Delete" and it sends a backspace key code! In short, for Macs we should - // treat backspace as delete, and it's harmless (and probably useful) to - // handle backspace for other platforms as well. - if (e.keyCode == SWT.BS) { - mDeleteAction.run(); - } else if (e.keyCode == SWT.ESC) { - mSelectionManager.selectParent(); - } else { - // Zooming actions - char c = e.character; - LayoutActionBar actionBar = mLayoutEditor.getGraphicalEditor() - .getLayoutActionBar(); - if (c == '1' && actionBar.isZoomingAllowed()) { - setScale(1, true); - } else if (c == '0' && actionBar.isZoomingAllowed()) { - setFitScale(true); - } else if (e.keyCode == '0' && (e.stateMask & SWT.MOD2) != 0 - && actionBar.isZoomingAllowed()) { - setFitScale(false); - } else if (c == '+' && actionBar.isZoomingAllowed()) { - actionBar.rescale(1); - } else if (c == '-' && actionBar.isZoomingAllowed()) { - actionBar.rescale(-1); - } - } - } - - public void keyReleased(KeyEvent e) { - } - }); - // --- setup drag'n'drop --- // DND Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html @@ -344,6 +307,36 @@ public class LayoutCanvas extends Canvas { } } + public void handleKeyPressed(KeyEvent e) { + // Set up backspace as an alias for the delete action within the canvas. + // On most Macs there is no delete key - though there IS a key labeled + // "Delete" and it sends a backspace key code! In short, for Macs we should + // treat backspace as delete, and it's harmless (and probably useful) to + // handle backspace for other platforms as well. + if (e.keyCode == SWT.BS) { + mDeleteAction.run(); + } else if (e.keyCode == SWT.ESC) { + mSelectionManager.selectParent(); + } else { + // Zooming actions + char c = e.character; + LayoutActionBar actionBar = mLayoutEditor.getGraphicalEditor() + .getLayoutActionBar(); + if (c == '1' && actionBar.isZoomingAllowed()) { + setScale(1, true); + } else if (c == '0' && actionBar.isZoomingAllowed()) { + setFitScale(true); + } else if (e.keyCode == '0' && (e.stateMask & SWT.MOD2) != 0 + && actionBar.isZoomingAllowed()) { + setFitScale(false); + } else if (c == '+' && actionBar.isZoomingAllowed()) { + actionBar.rescale(1); + } else if (c == '-' && actionBar.isZoomingAllowed()) { + actionBar.rescale(-1); + } + } + } + @Override public void dispose() { super.dispose(); @@ -438,7 +431,7 @@ public class LayoutCanvas extends Canvas { /** * Returns the {@link LayoutEditor} associated with this canvas. */ - LayoutEditor getLayoutEditor() { + public LayoutEditor getLayoutEditor() { return mLayoutEditor; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java index 8a2ab8c..5d48b5a 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java @@ -124,14 +124,41 @@ public class MoveGesture extends DropGesture { } @Override + public void begin(ControlPoint pos, int startMask) { + super.begin(pos, startMask); + + // Hide selection overlays during a move drag + mCanvas.getSelectionOverlay().setHidden(true); + } + + @Override public void end(ControlPoint pos, boolean canceled) { super.end(pos, canceled); + mCanvas.getSelectionOverlay().setHidden(false); + // Ensure that the outline is back to showing the current selection, since during // a drag gesture we temporarily set it to show the current target node instead. mCanvas.getSelectionManager().syncOutlineSelection(); } + /* TODO: Pass modifier mask to drag rules as well! This doesn't work yet since + the drag & drop code seems to steal keyboard events. + @Override + public boolean keyPressed(KeyEvent event) { + update(mCanvas.getGestureManager().getCurrentControlPoint()); + mCanvas.redraw(); + return true; + } + + @Override + public boolean keyReleased(KeyEvent event) { + update(mCanvas.getGestureManager().getCurrentControlPoint()); + mCanvas.redraw(); + return true; + } + */ + /* * The cursor has entered the drop target boundaries. * {@inheritDoc} @@ -440,6 +467,7 @@ public class MoveGesture extends DropGesture { df.sameCanvas = mCanvas == mGlobalDragInfo.getSourceCanvas(); df.invalidTarget = false; df.dipScale = mCanvas.getLayoutEditor().getGraphicalEditor().getDipScale(); + df.modifierMask = mCanvas.getGestureManager().getRuleModifierMask(); // Set the drag bounds, after converting it from control coordinates to // layout coordinates @@ -455,9 +483,12 @@ public class MoveGesture extends DropGesture { int y = (int) (controlDragBounds.y / verticalScale); int w = (int) (controlDragBounds.w / horizScale); int h = (int) (controlDragBounds.h / verticalScale); - dragBounds = new Rect(x, y, w, h); } + int baseline = dragInfo.getDragBaseline(); + if (baseline != -1) { + df.dragBaseline = baseline; + } df.dragBounds = dragBounds; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java index 4b4bf96..92ff59f 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java @@ -740,16 +740,14 @@ public class PaletteControl extends Composite { createDragImage(e); if (mImage != null && !mIsPlaceholder) { - int imageWidth = mImageLayoutBounds.width; - int imageHeight = mImageLayoutBounds.height; + int width = mImageLayoutBounds.width; + int height = mImageLayoutBounds.height; assert mImageLayoutBounds.x == 0; assert mImageLayoutBounds.y == 0; - LayoutCanvas canvas = mEditor.getCanvasControl(); - double scale = canvas.getScale(); - int x = -imageWidth / 2; - int y = -imageHeight / 2; - int width = (int) (imageWidth / scale); - int height = (int) (imageHeight / scale); + double scale = mEditor.getCanvasControl().getScale(); + + int x = (int) (-scale * width / 2); + int y = (int) (-scale * height / 2); bounds = new Rect(0, 0, width, height); dragBounds = new Rect(x, y, width, height); } @@ -773,6 +771,8 @@ public class PaletteControl extends Composite { null /* canvas */, null /* removeSource */); dragInfo.setDragBounds(dragBounds); + dragInfo.setDragBaseline(mBaseline); + e.doit = true; } @@ -813,12 +813,13 @@ public class PaletteControl extends Composite { private static final int MAX_RENDER_WIDTH = 500; /** Amount of alpha to multiply into the image (divided by 256) */ - private static final int IMG_ALPHA = 216; + private static final int IMG_ALPHA = 128; /** The image shown during the drag */ private Image mImage; /** The non-effect bounds of the drag image */ private Rectangle mImageLayoutBounds; + private int mBaseline = -1; /** * If true, the image is a preview of the view, and if not it is a "fallback" @@ -827,6 +828,7 @@ public class PaletteControl extends Composite { private boolean mIsPlaceholder; private void createDragImage(DragSourceEvent event) { + mBaseline = -1; Pair<Image, Rectangle> preview = renderPreview(); if (preview != null) { mImage = preview.getFirst(); @@ -872,11 +874,9 @@ public class PaletteControl extends Composite { if (!mIsPlaceholder) { // Shift the drag feedback image up such that it's centered under the // mouse pointer - - Rectangle imageBounds = mImage.getBounds(); - event.offsetX = imageBounds.width / 2; - event.offsetY = imageBounds.height / 2; - + double scale = mEditor.getCanvasControl().getScale(); + event.offsetX = (int) (scale * mImageLayoutBounds.width / 2); + event.offsetY = (int) (scale * mImageLayoutBounds.height / 2); } } @@ -1003,6 +1003,7 @@ public class PaletteControl extends Composite { if (viewInfoList != null && viewInfoList.size() > 0) { viewInfo = viewInfoList.get(0); + mBaseline = viewInfo.getBaseLine(); } if (viewInfo != null) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java index 7e5bc38..473b00a 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java @@ -264,12 +264,13 @@ public class PreviewIconFactory { model.loadFromXmlNode(document); RenderSession session = null; + NodeList childNodes = documentElement.getChildNodes(); try { LayoutLog logger = new RenderLogger("palette"); // Important to get these sizes large enough for clients that don't support // RenderMode.FULL_EXPAND such as 1.6 int width = 200; - int height = documentElement.getChildNodes().getLength() == 1 ? 400 : 1600; + int height = childNodes.getLength() == 1 ? 400 : 1600; Set<UiElementNode> expandNodes = Collections.<UiElementNode>emptySet(); RenderingMode renderingMode = RenderingMode.FULL_EXPAND; @@ -343,6 +344,23 @@ public class PreviewIconFactory { } } } else { + StringBuilder sb = new StringBuilder(); + for (int i = 0, n = childNodes.getLength(); i < n; i++) { + Node node = childNodes.item(i); + if (node instanceof Element) { + Element e = (Element) node; + String fqn = repository.getFullClassName(e); + fqn = fqn.substring(fqn.lastIndexOf('.') + 1); + if (sb.length() > 0) { + sb.append(", "); //$NON-NLS-1$ + } + sb.append(fqn); + } + } + AdtPlugin.log(IStatus.WARNING, "Failed to render set of icons for %1$s", + sb.toString()); + System.out.println(sb.toString()); + if (session.getResult().getException() != null) { AdtPlugin.log(session.getResult().getException(), session.getResult().getErrorMessage()); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java index f2af726..1714828 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java @@ -17,13 +17,14 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import com.android.ide.common.api.DropFeedback; -import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; import com.android.ide.common.api.ResizePolicy; +import com.android.ide.common.api.SegmentType; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.Position; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; +import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.graphics.GC; import java.util.Collections; @@ -47,6 +48,8 @@ public class ResizeGesture extends Gesture { private NodeProxy mChildNode; private DropFeedback mFeedback; private ResizePolicy mResizePolicy; + private SegmentType mHorizontalEdge; + private SegmentType mVerticalEdge; /** * Creates a new marquee selection (selection swiping). @@ -62,26 +65,45 @@ public class ResizeGesture extends Gesture { mChildNode = item.getNode(); mParentNode = (NodeProxy) mChildNode.getParent(); mResizePolicy = item.getResizePolicy(); + mHorizontalEdge = getHorizontalEdgeType(mHandle); + mVerticalEdge = getVerticalEdgeType(mHandle); } @Override public void begin(ControlPoint pos, int startMask) { super.begin(pos, startMask); + mCanvas.getSelectionOverlay().setHidden(true); + RulesEngine rulesEngine = mCanvas.getRulesEngine(); - Point where = pos.toLayout().toPoint(); Rect newBounds = getNewBounds(pos); - mFeedback = rulesEngine.callOnResizeBegin(mChildNode, mParentNode, where, newBounds); + mFeedback = rulesEngine.callOnResizeBegin(mChildNode, mParentNode, newBounds, + mHorizontalEdge, mVerticalEdge); mCanvas.getGestureManager().updateMessage(mFeedback); } @Override + public boolean keyPressed(KeyEvent event) { + update(mCanvas.getGestureManager().getCurrentControlPoint()); + mCanvas.redraw(); + return true; + } + + @Override + public boolean keyReleased(KeyEvent event) { + update(mCanvas.getGestureManager().getCurrentControlPoint()); + mCanvas.redraw(); + return true; + } + + @Override public void update(ControlPoint pos) { super.update(pos); RulesEngine rulesEngine = mCanvas.getRulesEngine(); - Point where = pos.toLayout().toPoint(); Rect newBounds = getNewBounds(pos); - rulesEngine.callOnResizeUpdate(mFeedback, mChildNode, mParentNode, where, newBounds); + int modifierMask = mCanvas.getGestureManager().getRuleModifierMask(); + rulesEngine.callOnResizeUpdate(mFeedback, mChildNode, mParentNode, newBounds, + modifierMask); mCanvas.getGestureManager().updateMessage(mFeedback); } @@ -91,10 +113,11 @@ public class ResizeGesture extends Gesture { if (!canceled) { RulesEngine rulesEngine = mCanvas.getRulesEngine(); - Point where = pos.toLayout().toPoint(); Rect newBounds = getNewBounds(pos); - rulesEngine.callOnResizeEnd(mFeedback, mChildNode, mParentNode, where, newBounds); + rulesEngine.callOnResizeEnd(mFeedback, mChildNode, mParentNode, newBounds); } + + mCanvas.getSelectionOverlay().setHidden(false); } /** @@ -185,6 +208,43 @@ public class ResizeGesture extends Gesture { return new Rect(x, y, w, h); } + private static SegmentType getHorizontalEdgeType(SelectionHandle handle) { + switch (handle.getPosition()) { + case BOTTOM_LEFT: + case BOTTOM_RIGHT: + case BOTTOM_MIDDLE: + return SegmentType.BOTTOM; + case LEFT_MIDDLE: + case RIGHT_MIDDLE: + return null; + case TOP_LEFT: + case TOP_MIDDLE: + case TOP_RIGHT: + return SegmentType.TOP; + default: assert false : handle.getPosition(); + } + return null; + } + + private static SegmentType getVerticalEdgeType(SelectionHandle handle) { + switch (handle.getPosition()) { + case TOP_LEFT: + case LEFT_MIDDLE: + case BOTTOM_LEFT: + return SegmentType.LEFT; + case BOTTOM_MIDDLE: + case TOP_MIDDLE: + return null; + case TOP_RIGHT: + case RIGHT_MIDDLE: + case BOTTOM_RIGHT: + return SegmentType.RIGHT; + default: assert false : handle.getPosition(); + } + return null; + } + + @Override public List<Overlay> createOverlays() { mOverlay = new ResizeOverlay(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java index 5e01bf2..4afb123 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java @@ -182,7 +182,7 @@ class SelectionItem { * @return the {@link ResizePolicy} for this item, never null */ public ResizePolicy getResizePolicy() { - if (mResizePolicy == null) { + if (mResizePolicy == null && mNodeProxy != null) { mResizePolicy = ViewMetadataRepository.get().getResizePolicy(mNodeProxy.getFqcn()); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java index ff8f4be..01e72d0 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java @@ -686,7 +686,7 @@ public class SelectionManager implements ISelectionProvider { } /** Sync the selection with an updated view info tree */ - /* package */ void sync(CanvasViewInfo lastValidViewInfoRoot) { + /* package */ void sync() { // Check if the selection is still the same (based on the object keys) // and eventually recompute their bounds. for (ListIterator<SelectionItem> it = mSelections.listIterator(); it.hasNext(); ) { @@ -694,8 +694,8 @@ public class SelectionManager implements ISelectionProvider { // Check if the selected object still exists ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy(); - Object key = s.getViewInfo().getUiViewNode(); - CanvasViewInfo vi = viewHierarchy.findViewInfoKey(key, lastValidViewInfoRoot); + UiViewElementNode key = s.getViewInfo().getUiViewNode(); + CanvasViewInfo vi = viewHierarchy.findViewInfoFor(key); // Remove the previous selection -- if the selected object still exists // we need to recompute its bounds in case it moved so we'll insert a new one @@ -905,7 +905,7 @@ public class SelectionManager implements ISelectionProvider { boolean haveSelection = selections.size() > 0; Action a; - a = selectionManager.new SelectAction("Select Parent", SELECT_PARENT); + a = selectionManager.new SelectAction("Select Parent\tEsc", SELECT_PARENT); new ActionContributionItem(a).fill(menu, -1); a.setEnabled(notRoot); a.setAccelerator(SWT.ESC); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java index 5d8b53d..b3cc13b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java @@ -25,13 +25,18 @@ import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; import org.eclipse.swt.graphics.GC; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * The {@link SelectionOverlay} paints the current selection as an overlay. */ public class SelectionOverlay extends Overlay { private final LayoutCanvas mCanvas; + private boolean mHidden; /** * Constructs a new {@link SelectionOverlay} tied to the given canvas. @@ -43,6 +48,17 @@ public class SelectionOverlay extends Overlay { } /** + * Set whether the selection overlay should be hidden. This is done during some + * gestures like resize where the new bounds could be confused with the current + * selection bounds. + * + * @param hidden when true, hide the selection bounds, when false, unhide. + */ + public void setHidden(boolean hidden) { + mHidden = hidden; + } + + /** * Paints the selection. * * @param selectionManager The {@link SelectionManager} holding the @@ -53,9 +69,14 @@ public class SelectionOverlay extends Overlay { */ public void paint(SelectionManager selectionManager, GCWrapper gcWrapper, GC gc, RulesEngine rulesEngine) { + if (mHidden) { + return; + } + List<SelectionItem> selections = selectionManager.getSelections(); int n = selections.size(); if (n > 0) { + List<NodeProxy> selectedNodes = new ArrayList<NodeProxy>(); boolean isMultipleSelection = n > 1; for (SelectionItem s : selections) { if (s.isRoot()) { @@ -66,6 +87,18 @@ public class SelectionOverlay extends Overlay { NodeProxy node = s.getNode(); if (node != null) { paintSelection(gcWrapper, gc, s, isMultipleSelection); + selectedNodes.add(node); + } + } + + if (selectedNodes.size() > 0) { + paintSelectionFeedback(gcWrapper, selectedNodes, rulesEngine); + } else { + CanvasViewInfo root = mCanvas.getViewHierarchy().getRoot(); + if (root != null) { + NodeProxy parent = mCanvas.getNodeFactory().create(root); + rulesEngine.callPaintSelectionFeedback(gcWrapper, + parent, Collections.<INode>emptyList()); } } @@ -75,6 +108,13 @@ public class SelectionOverlay extends Overlay { paintHints(gcWrapper, node, rulesEngine); } } + } else { + CanvasViewInfo root = mCanvas.getViewHierarchy().getRoot(); + if (root != null) { + NodeProxy parent = mCanvas.getNodeFactory().create(root); + rulesEngine.callPaintSelectionFeedback(gcWrapper, + parent, Collections.<INode>emptyList()); + } } } @@ -113,6 +153,32 @@ public class SelectionOverlay extends Overlay { } } + private void paintSelectionFeedback(GCWrapper gcWrapper, List<NodeProxy> nodes, + RulesEngine rulesEngine) { + // Add fastpath for n=1 + + // Group nodes into parent/child groups + Set<INode> parents = new HashSet<INode>(); + for (INode node : nodes) { + INode parent = node.getParent(); + if (/*parent == null || */parent instanceof NodeProxy) { + NodeProxy parentNode = (NodeProxy) parent; + parents.add(parentNode); + } + } + for (INode parent : parents) { + List<INode> children = new ArrayList<INode>(); + for (INode node : nodes) { + INode nodeParent = node.getParent(); + if (nodeParent == parent) { + children.add(node); + } + } + rulesEngine.callPaintSelectionFeedback(gcWrapper, + (NodeProxy) parent, children); + } + } + /** Called by the canvas when a view is being selected. */ private void paintSelection(IGraphics gc, GC swtGc, SelectionItem item, boolean isMultipleSelection) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java index 6eefa2d..90e6228 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java @@ -39,7 +39,17 @@ public enum SwtDrawingStyle { /** * The style definition corresponding to {@link DrawingStyle#GUIDELINE} */ - GUIDELINE(new RGB(0x00, 0xFF, 0x00), 255, SWT.LINE_DOT), + GUIDELINE(new RGB(0x00, 0xAA, 0x00), 192, SWT.LINE_SOLID), + + /** + * The style definition corresponding to {@link DrawingStyle#GUIDELINE} + */ + GUIDELINE_SHADOW(new RGB(0x00, 0xAA, 0x00), 192, SWT.LINE_SOLID), + + /** + * The style definition corresponding to {@link DrawingStyle#GUIDELINE_DASHED} + */ + GUIDELINE_DASHED(new RGB(0x00, 0xAA, 0x00), 192, SWT.LINE_CUSTOM), /** * The style definition corresponding to {@link DrawingStyle#HOVER} @@ -85,6 +95,17 @@ public enum SwtDrawingStyle { DROP_PREVIEW(new RGB(0xFF, 0x99, 0x00), 255, null, 0, 2, SWT.LINE_CUSTOM), /** + * The style definition corresponding to {@link DrawingStyle#RESIZE_PREVIEW} + */ + RESIZE_PREVIEW(new RGB(0xFF, 0x99, 0x00), 255, null, 0, 2, SWT.LINE_SOLID), + + /** + * The style used to show a proposed resize bound which is being rejected (for example, + * because there is no near edge to attach to in a RelativeLayout). + */ + RESIZE_FAIL(new RGB(0xFF, 0x99, 0x00), 255, null, 0, 2, SWT.LINE_CUSTOM), + + /** * The style definition corresponding to {@link DrawingStyle#HELP} */ HELP(new RGB(0xFF, 0xFF, 0xFF), 255, new RGB(0x00, 0x00, 0x00), 128, 1, SWT.LINE_SOLID), @@ -92,7 +113,22 @@ public enum SwtDrawingStyle { /** * The style definition corresponding to {@link DrawingStyle#INVALID} */ - INVALID(new RGB(0xFF, 0xFF, 0xFF), 255, new RGB(0xFF, 0x00, 0x00), 150, 2, SWT.LINE_SOLID), + INVALID(new RGB(0xFF, 0xFF, 0xFF), 255, new RGB(0xFF, 0x00, 0x00), 64, 2, SWT.LINE_SOLID), + + /** + * The style definition corresponding to {@link DrawingStyle#DEPENDENCY} + */ + DEPENDENCY(new RGB(0xFF, 0xFF, 0xFF), 255, new RGB(0xFF, 0xFF, 0x00), 24, 2, SWT.LINE_SOLID), + + /** + * The style definition corresponding to {@link DrawingStyle#CYCLE} + */ + CYCLE(new RGB(0xFF, 0x00, 0x00), 192, null, 0, 1, SWT.LINE_SOLID), + + /** + * The style definition corresponding to {@link DrawingStyle#DRAGGED} + */ + DRAGGED(new RGB(0xFF, 0xFF, 0xFF), 255, new RGB(0x00, 0xFF, 0x00), 16, 2, SWT.LINE_SOLID), /** * The style definition corresponding to {@link DrawingStyle#EMPTY} @@ -202,6 +238,10 @@ public enum SwtDrawingStyle { return SELECTION; case GUIDELINE: return GUIDELINE; + case GUIDELINE_SHADOW: + return GUIDELINE_SHADOW; + case GUIDELINE_DASHED: + return GUIDELINE_DASHED; case HOVER: return HOVER; case HOVER_SELECTION: @@ -218,10 +258,20 @@ public enum SwtDrawingStyle { return DROP_RECIPIENT; case DROP_PREVIEW: return DROP_PREVIEW; + case RESIZE_PREVIEW: + return RESIZE_PREVIEW; + case RESIZE_FAIL: + return RESIZE_FAIL; case HELP: return HELP; case INVALID: return INVALID; + case DEPENDENCY: + return DEPENDENCY; + case CYCLE: + return CYCLE; + case DRAGGED: + return DRAGGED; case EMPTY: return EMPTY; case CUSTOM1: diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java index 10625bb..50dfb73 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java @@ -34,8 +34,10 @@ import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.RandomAccess; import java.util.Set; @@ -110,6 +112,9 @@ public class ViewHierarchy { /** The render session for the current view hierarchy */ private RenderSession mSession; + /** Map from nodes to canvas view infos */ + private Map<UiViewElementNode, CanvasViewInfo> mNodeToView = Collections.emptyMap(); + /** * Disposes the view hierarchy content. */ @@ -146,6 +151,7 @@ public class ViewHierarchy { mSession = session; mIsResultValid = (session != null && session.getResult().isSuccess()); mExplodedParents = false; + mNodeToView = new HashMap<UiViewElementNode, CanvasViewInfo>(50); if (mIsResultValid && session != null) { List<ViewInfo> rootList = session.getRootViews(); @@ -213,7 +219,7 @@ public class ViewHierarchy { addInvisibleParents(mLastValidViewInfoRoot, explodedNodes); // Update the selection - mCanvas.getSelectionManager().sync(mLastValidViewInfoRoot); + mCanvas.getSelectionManager().sync(); } else { mIncludedBounds = null; mInvisibleParents.clear(); @@ -268,6 +274,7 @@ public class ViewHierarchy { if (key != null) { mCanvas.getNodeFactory().create(vi); + mNodeToView.put(key, vi); } for (CanvasViewInfo child : vi.getChildren()) { @@ -568,10 +575,7 @@ public class ViewHierarchy { * null if no match was found. */ public CanvasViewInfo findViewInfoFor(INode node) { - if (mLastValidViewInfoRoot != null && node instanceof NodeProxy) { - return findViewInfoKey(((NodeProxy) node).getNode(), mLastValidViewInfoRoot); - } - return null; + return findViewInfoFor((NodeProxy) node); } /** @@ -580,27 +584,24 @@ public class ViewHierarchy { * * @param viewKey The view key that a matching {@link CanvasViewInfo} should * have as its key. - * @param canvasViewInfo A root {@link CanvasViewInfo} to search from. * @return A {@link CanvasViewInfo} matching the given key, or null if not * found. */ - public CanvasViewInfo findViewInfoKey(Object viewKey, CanvasViewInfo canvasViewInfo) { - if (canvasViewInfo == null) { - return null; - } - if (canvasViewInfo.getUiViewNode() == viewKey) { - return canvasViewInfo; - } - - // try to find a matching child - for (CanvasViewInfo child : canvasViewInfo.getChildren()) { - CanvasViewInfo v = findViewInfoKey(viewKey, child); - if (v != null) { - return v; - } - } + public CanvasViewInfo findViewInfoFor(UiElementNode viewKey) { + return mNodeToView.get(viewKey); + } - return null; + /** + * Tries to find a child with the given node proxy as the view key. + * Returns null if not found. + * + * @param proxy The view key that a matching {@link CanvasViewInfo} should + * have as its key. + * @return A {@link CanvasViewInfo} matching the given key, or null if not + * found. + */ + public CanvasViewInfo findViewInfoFor(NodeProxy proxy) { + return mNodeToView.get(proxy.getNode()); } /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java index e608377..2b5e28c 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java @@ -18,6 +18,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gre; import com.android.ide.common.api.INode; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; @@ -33,8 +34,10 @@ public class NodeFactory { private final HashMap<UiViewElementNode, NodeProxy> mNodeMap = new HashMap<UiViewElementNode, NodeProxy>(); + private LayoutCanvas mCanvas; - public NodeFactory() { + public NodeFactory(LayoutCanvas canvas) { + mCanvas = canvas; } /** @@ -58,6 +61,10 @@ public class NodeFactory { mNodeMap.clear(); } + public LayoutCanvas getCanvas() { + return mCanvas; + } + //---- private NodeProxy create(UiViewElementNode uiNode, Rectangle bounds) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java index 72cc32a..c4c050a 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java @@ -19,6 +19,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gre; import com.android.ide.common.api.IAttributeInfo; import com.android.ide.common.api.INode; import com.android.ide.common.api.INodeHandler; +import com.android.ide.common.api.Margins; import com.android.ide.common.api.Rect; import com.android.ide.common.resources.platform.AttributeInfo; import com.android.ide.eclipse.adt.AdtPlugin; @@ -28,8 +29,10 @@ import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleAttribute; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; @@ -46,7 +49,7 @@ import java.util.List; * */ public class NodeProxy implements INode { - + private static final Margins NO_MARGINS = new Margins(0, 0, 0, 0); private final UiViewElementNode mNode; private final Rect mBounds; private final NodeFactory mFactory; @@ -80,6 +83,26 @@ public class NodeProxy implements INode { return mBounds; } + public Margins getMargins() { + ViewHierarchy viewHierarchy = mFactory.getCanvas().getViewHierarchy(); + CanvasViewInfo view = viewHierarchy.findViewInfoFor(this); + if (view != null) { + return view.getMargins(); + } + + return NO_MARGINS; + } + + + public int getBaseline() { + ViewHierarchy viewHierarchy = mFactory.getCanvas().getViewHierarchy(); + CanvasViewInfo view = viewHierarchy.findViewInfoFor(this); + if (view != null) { + return view.getBaseline(); + } + + return -1; + } /** * Updates the bounds of this node proxy. Bounds cannot be null, but it can be invalid. @@ -101,9 +124,11 @@ public class NodeProxy implements INode { } public String getFqcn() { - ElementDescriptor desc = mNode.getDescriptor(); - if (desc instanceof ViewElementDescriptor) { - return ((ViewElementDescriptor) desc).getFullClassName(); + if (mNode != null) { + ElementDescriptor desc = mNode.getDescriptor(); + if (desc instanceof ViewElementDescriptor) { + return ((ViewElementDescriptor) desc).getFullClassName(); + } } return null; } @@ -359,6 +384,10 @@ public class NodeProxy implements INode { } + @Override + public String toString() { + return "NodeProxy [node=" + mNode + ", bounds=" + mBounds + "]"; + } // --- internal helpers --- diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java index c4a88f9..a76845b 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java @@ -33,13 +33,16 @@ import com.android.ide.common.api.InsertType; import com.android.ide.common.api.MenuAction; 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.ViewRule; import com.android.ide.common.resources.ResourceRepository; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.actions.AddCompatibilityJarAction; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GCWrapper; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager; @@ -102,6 +105,7 @@ import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -332,6 +336,23 @@ public class RulesEngine { return null; } + public void callPaintSelectionFeedback(GCWrapper gcWrapper, NodeProxy parentNode, + List<? extends INode> childNodes) { + // try to find a rule for this element's FQCN + IViewRule rule = loadRule(parentNode.getNode()); + + if (rule != null) { + try { + rule.paintSelectionFeedback(gcWrapper, parentNode, childNodes); + + } catch (Exception e) { + AdtPlugin.log(e, "%s.callPaintSelectionFeedback() failed: %s", + rule.getClass().getSimpleName(), + e.toString()); + } + } + } + /** * Called when the d'n'd starts dragging over the target node. * If interested, returns a DropFeedback passed to onDrop/Move/Leave/Paint. @@ -469,13 +490,13 @@ public class RulesEngine { // ---- Resize operations ---- - public DropFeedback callOnResizeBegin(NodeProxy child, NodeProxy parent, Point where, - Rect newBounds) { + public DropFeedback callOnResizeBegin(NodeProxy child, NodeProxy parent, Rect newBounds, + SegmentType horizontalEdge, SegmentType verticalEdge) { IViewRule rule = loadRule(parent.getNode()); if (rule != null) { try { - return rule.onResizeBegin(child, parent); + return rule.onResizeBegin(child, parent, horizontalEdge, verticalEdge); } catch (Exception e) { AdtPlugin.log(e, "%s.onResizeBegin() failed: %s", rule.getClass().getSimpleName(), e.toString()); @@ -485,13 +506,13 @@ public class RulesEngine { return null; } - public void callOnResizeUpdate(DropFeedback feedback, NodeProxy child, - NodeProxy parent, Point where, Rect newBounds) { + public void callOnResizeUpdate(DropFeedback feedback, NodeProxy child, NodeProxy parent, + Rect newBounds, int modifierMask) { IViewRule rule = loadRule(parent.getNode()); if (rule != null) { try { - rule.onResizeUpdate(feedback, child, parent, newBounds); + rule.onResizeUpdate(feedback, child, parent, newBounds, modifierMask); } catch (Exception e) { AdtPlugin.log(e, "%s.onResizeUpdate() failed: %s", rule.getClass().getSimpleName(), e.toString()); @@ -500,7 +521,7 @@ public class RulesEngine { } public void callOnResizeEnd(DropFeedback feedback, NodeProxy child, NodeProxy parent, - Point where, Rect newBounds) { + Rect newBounds) { IViewRule rule = loadRule(parent.getNode()); if (rule != null) { @@ -1179,6 +1200,29 @@ public class RulesEngine { } return null; } + + public void redraw() { + mEditor.getCanvasControl().redraw(); + } + + public void layout() { + mEditor.recomputeLayout(); + } + + public Map<INode, Rect> measureChildren(INode parent, + IClientRulesEngine.AttributeFilter filter) { + Map<INode, Rect> map = mEditor.measureChildren(parent, filter); + if (map == null) { + map = Collections.emptyMap(); + } + return map; + } + + public int pxToDp(int px) { + ConfigurationComposite config = mEditor.getConfigurationComposite(); + float dpi = config.getDensity().getDpiValue(); + return (int) (px * 160 / dpi); + } } private String createNewFragmentClass(IJavaProject javaProject) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml index 9c77119..3ce531d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml @@ -90,7 +90,8 @@ resize="horizontal" fill="width_in_vertical" /> <view - class="android.widget.QuickContactBadge" /> + class="android.widget.QuickContactBadge" + resize="scaled" /> <view class="android.widget.RadioGroup" /> <view @@ -103,7 +104,7 @@ class="android.widget.EditText" name="Plain Text" init="" - resize="horizontal" + resize="full" relatedTo="Spinner,TextView,AutoCompleteTextView,MultiAutoCompleteTextView" fill="width_in_vertical"> <view diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java index 47f22a1..0e780d1 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java @@ -143,12 +143,12 @@ public class UiElementNode implements IPropertySource { @Override public String toString() { - return String.format("%s [desc: %s, parent: %s, children: %d]", //$NON-NLS-1$ - this.getClass().getSimpleName(), - mDescriptor, - mUiParent != null ? mUiParent.toString() : "none", //$NON-NLS-1$ - mUiChildren != null ? mUiChildren.size() : 0 - ); + return String.format("%s [desc: %s, parent: %s, children: %d]", //$NON-NLS-1$ + this.getClass().getSimpleName(), + mDescriptor, + mUiParent != null ? mUiParent.toString() : "none", //$NON-NLS-1$ + mUiChildren != null ? mUiChildren.size() : 0 + ); } /** diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/api/RectTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/api/RectTest.java index 36fa743..784ad8f 100755 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/api/RectTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/api/RectTest.java @@ -217,7 +217,7 @@ public class RectTest extends TestCase { public final void testToString() { Rect r = new Rect(3, 4, 20, 30); - assertEquals("Rect [3x4 - 20x30]", r.toString()); + assertEquals("Rect [(3,4)-(23,34): 20x30]", r.toString()); } public final void testEqualsObject() { @@ -255,4 +255,18 @@ public class RectTest extends TestCase { } + public final void testCenter() { + Rect r = new Rect(10, 20, 30, 40); + Point center = r.center(); + assertEquals(25, center.x); + assertEquals(40, center.y); + assertEquals(25, r.centerX()); + assertEquals(40, r.centerY()); + } + + public final void testX2Y2() { + Rect r = new Rect(1, 2, 3, 4); + assertEquals(4, r.x2()); + assertEquals(6, r.y2()); + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java index a3d0f1b..b59f2f9 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java @@ -33,6 +33,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import junit.framework.TestCase; @@ -258,6 +259,23 @@ public class LayoutTestBase extends TestCase { fail("Not supported in tests yet"); return null; } + + public void layout() { + fail("Not supported in tests yet"); + } + + public void redraw() { + fail("Not supported in tests yet"); + } + + public Map<INode, Rect> measureChildren(INode parent, AttributeFilter filter) { + return null; + } + + public int pxToDp(int px) { + fail("Not supported in tests yet"); + return px; + } } public void testDummy() { diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LinearLayoutRuleTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LinearLayoutRuleTest.java index fb61186..eb2158e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LinearLayoutRuleTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LinearLayoutRuleTest.java @@ -367,6 +367,19 @@ public class LinearLayoutRuleTest extends LayoutTestBase { "seStyle(DROP_PREVIEW), drawRect(Rect[0,381,100,80])"); } + + public void testFormatFloatValue() throws Exception { + assertEquals("1", LinearLayoutRule.formatFloatAttribute(1.0f)); + assertEquals("2", LinearLayoutRule.formatFloatAttribute(2.0f)); + assertEquals("1.50", LinearLayoutRule.formatFloatAttribute(1.5f)); + assertEquals("1.50", LinearLayoutRule.formatFloatAttribute(1.50f)); + assertEquals("1.51", LinearLayoutRule.formatFloatAttribute(1.51f)); + assertEquals("1.51", LinearLayoutRule.formatFloatAttribute(1.514542f)); + assertEquals("1.52", LinearLayoutRule.formatFloatAttribute(1.516542f)); + assertEquals("-1.51", LinearLayoutRule.formatFloatAttribute(-1.51f)); + assertEquals("-1", LinearLayoutRule.formatFloatAttribute(-1f)); + } + // Left to test: // Check inserting at last pos with multiple children // Check inserting with no bounds rectangle for dragged element diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/RelativeLayoutRuleTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/RelativeLayoutRuleTest.java index c0fa548..09cd8cc 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/RelativeLayoutRuleTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/RelativeLayoutRuleTest.java @@ -21,10 +21,6 @@ import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; import com.android.ide.common.api.INode; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; -import com.android.util.Pair; - -import java.util.ArrayList; -import java.util.List; /** Test the {@link RelativeLayoutRule} */ public class RelativeLayoutRuleTest extends LayoutTestBase { @@ -61,6 +57,7 @@ public class RelativeLayoutRuleTest extends LayoutTestBase { currentIndex, combined); } + /* This needs to be updated for the new interaction public void testDropTopEdge() { // If we drag right into the button itself, not a valid drop position INode inserted = dragInto( @@ -165,4 +162,5 @@ public class RelativeLayoutRuleTest extends LayoutTestBase { } // TODO: Test error (dragging on ancestor) + */ } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestGraphics.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestGraphics.java index b82f309..5088bac 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestGraphics.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestGraphics.java @@ -142,6 +142,10 @@ public class TestGraphics implements IGraphics { mDrawn.add("useStyle(" + style + ")"); } + public void drawArrow(int x1, int y1, int x2, int y2, int size) { + mDrawn.add("drawArrow(" + x1 + "," + y1 + "," + x2 + "," + y2 + ")"); + } + private static String rectToString(Rect rect) { return "Rect[" + rect.x + "," + rect.y + "," + rect.w + "," + rect.h + "]"; } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java index d5f1ae9..7d77252 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java @@ -21,6 +21,7 @@ import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; import com.android.ide.common.api.IAttributeInfo; import com.android.ide.common.api.INode; import com.android.ide.common.api.INodeHandler; +import com.android.ide.common.api.Margins; import com.android.ide.common.api.Rect; import java.util.ArrayList; @@ -172,4 +173,11 @@ public class TestNode implements INode { + ", attributes=" + mAttributes + ", bounds=" + mBounds + "]"; } + public int getBaseline() { + return -1; + } + + public Margins getMargins() { + return null; + } }
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilitiesTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilitiesTest.java index 049e1cc..9947b2f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilitiesTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilitiesTest.java @@ -115,5 +115,4 @@ public class DomUtilitiesTest extends TestCase { assertFalse(DomUtilities.isContiguous(Arrays.asList(foo, baz))); assertFalse(DomUtilities.isContiguous(Arrays.asList(root, baz))); } - } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLoggerTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLoggerTest.java index 300dfb6..e9f07aa 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLoggerTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLoggerTest.java @@ -30,7 +30,7 @@ public class RenderLoggerTest extends TestCase { l.fidelityWarning(null, "No perspective Transforms", null, null); l.fidelityWarning(null, "No GPS", null, null); assertTrue(l.hasProblems()); - assertEquals("The graphics preview may not be accurate:\n" + assertEquals("The graphics preview in the layout editor may not be accurate:\n" + "* No perspective Transforms\n" + "* No GPS\n", l.getProblems()); assertFalse(l.seenTag("foo")); assertFalse(l.seenTag(null)); diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactoryTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactoryTest.java index 9f670cc..0e6d33d 100755 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactoryTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactoryTest.java @@ -34,7 +34,7 @@ public class NodeFactoryTest extends TestCase { @Override protected void setUp() throws Exception { super.setUp(); - m = new NodeFactory(); + m = new NodeFactory(null); } |