aboutsummaryrefslogtreecommitdiffstats
path: root/eclipse
diff options
context:
space:
mode:
authorTor Norbye <tnorbye@google.com>2011-04-10 08:18:45 -0700
committerTor Norbye <tnorbye@google.com>2011-06-01 18:14:32 -0700
commit80d9301c2e874b29889c41adb0623666cf534fa0 (patch)
treea05cd9083d275427a83e5957b1763f87309e36ed /eclipse
parent429ae88878cf781753d8261d350ad89fe5864169 (diff)
downloadsdk-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')
-rw-r--r--eclipse/dictionary.txt6
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DrawingStyle.java40
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DropFeedback.java18
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IClientRulesEngine.java56
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IGraphics.java11
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/INode.java14
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewRule.java25
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Margins.java56
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Rect.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Segment.java69
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/SegmentType.java93
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java38
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java235
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java65
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java1
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java265
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java834
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableLayoutRule.java21
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableRowRule.java10
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/centerHorizontally.pngbin0 -> 505 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/centerVertically.pngbin0 -> 501 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/constraints.pngbin0 -> 507 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ConstraintPainter.java783
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ConstraintType.java236
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java322
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java765
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelinePainter.java186
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/MarginType.java52
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/Match.java73
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/MoveHandler.java284
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ResizeHandler.java255
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/structure.pngbin0 -> 546 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java6
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java49
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java119
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java8
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java91
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GlobalCanvasDragInfo.java20
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java125
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java71
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java33
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java29
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java20
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java74
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java8
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java66
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java54
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java45
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java9
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java37
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java58
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml5
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java12
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/api/RectTest.java16
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java18
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LinearLayoutRuleTest.java13
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/RelativeLayoutRuleTest.java6
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestGraphics.java4
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java8
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilitiesTest.java1
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLoggerTest.java2
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactoryTest.java2
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
new file mode 100644
index 0000000..5053cda
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/centerHorizontally.png
Binary files differ
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
new file mode 100644
index 0000000..ebba8e8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/centerVertically.png
Binary files differ
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
new file mode 100644
index 0000000..7247d5a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/constraints.png
Binary files differ
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
new file mode 100644
index 0000000..e5d7538
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/structure.png
Binary files differ
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 &amp; 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);
}