aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsListViewRule.java28
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java254
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AdapterViewRule.java62
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java878
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java996
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/CalendarViewRule.java44
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/DatePickerRule.java22
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/DialerFilterRule.java63
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/EditTextRule.java139
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FragmentRule.java46
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FrameLayoutRule.java195
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GravityHelper.java233
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java676
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridViewRule.java41
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/HorizontalScrollViewRule.java96
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/IgnoredLayoutRule.java46
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ImageButtonRule.java54
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ImageViewRule.java57
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/IncludeRule.java46
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java1092
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ListViewRule.java42
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MapViewRule.java46
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MergeRule.java38
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/PropertyCallback.java82
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/PropertySettingNodeHandler.java42
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/QuickContactBadgeRule.java34
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RadioGroupRule.java50
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java413
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ResizeState.java131
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ScrollViewRule.java96
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/SeekBarRule.java42
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/SlidingDrawerRule.java68
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TabHostRule.java82
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TabWidgetRule.java27
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableLayoutRule.java218
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableRowRule.java80
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TimePickerRule.java27
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ViewRule.java31
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ViewTagRule.java49
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/WebViewRule.java46
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ZoomButtonRule.java35
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ZoomControlsRule.java27
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/addcol.pngbin0 -> 479 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/addrow.pngbin0 -> 442 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/allweight.pngbin0 -> 460 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/baseline.pngbin0 -> 602 bytes
-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/clearweights.pngbin0 -> 573 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/distribute.pngbin0 -> 460 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/fillheight.pngbin0 -> 483 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/fillwidth.pngbin0 -> 476 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/gravity.pngbin0 -> 563 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java840
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridLayoutPainter.java370
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridMatch.java154
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridModel.java2384
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/gridmode.pngbin0 -> 519 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/hlinear.pngbin0 -> 403 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/margins.pngbin0 -> 503 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.java241
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DeletionHandler.java267
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java326
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java839
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelinePainter.java208
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/Match.java100
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/MoveHandler.java299
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ResizeHandler.java265
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/removecol.pngbin0 -> 469 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/removerow.pngbin0 -> 442 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/showgrid.pngbin0 -> 477 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/snap.pngbin0 -> 575 bytes
-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/common/layout/vlinear.pngbin0 -> 369 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/weights.pngbin0 -> 520 bytes
77 files changed, 13850 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsListViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsListViewRule.java
new file mode 100644
index 000000000..cd1b0fcae
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsListViewRule.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2010 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;
+
+import com.android.ide.common.api.IViewRule;
+
+/**
+ * An {@link IViewRule} for android.widget.AbsListViewRule
+ */
+public class AbsListViewRule extends IgnoredLayoutRule {
+
+ // GridViews and ListViews are not configurable via XML
+
+}
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
new file mode 100644
index 000000000..3ec3b5f1a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_X;
+import static com.android.SdkConstants.ATTR_LAYOUT_Y;
+import static com.android.SdkConstants.VALUE_N_DP;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.DropFeedback;
+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.INode;
+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.utils.Pair;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An {@link IViewRule} for android.widget.AbsoluteLayout and all its derived
+ * classes.
+ */
+public class AbsoluteLayoutRule extends BaseLayoutRule {
+
+ @Override
+ public List<String> getSelectionHint(@NonNull INode parentNode, @NonNull INode childNode) {
+ List<String> infos = new ArrayList<String>(2);
+ infos.add("AbsoluteLayout is deprecated.");
+ infos.add("Use other layouts instead.");
+ return infos;
+ }
+
+ // ==== Drag'n'drop support ====
+ // The AbsoluteLayout accepts any drag'n'drop anywhere on its surface.
+
+ @Override
+ public DropFeedback onDropEnter(@NonNull INode targetNode, @Nullable Object targetView,
+ final @Nullable IDragElement[] elements) {
+
+ if (elements.length == 0) {
+ return null;
+ }
+
+ DropFeedback df = new DropFeedback(null, new IFeedbackPainter() {
+ @Override
+ public void paint(@NonNull IGraphics gc, @NonNull INode node,
+ @NonNull DropFeedback feedback) {
+ // Paint callback for the AbsoluteLayout.
+ // This is called by the canvas when a draw is needed.
+ drawFeedback(gc, node, elements, feedback);
+ }
+ });
+ df.errorMessage = "AbsoluteLayout is deprecated.";
+ return df;
+ }
+
+ void drawFeedback(
+ IGraphics gc,
+ INode targetNode,
+ IDragElement[] elements,
+ DropFeedback feedback) {
+ Rect b = targetNode.getBounds();
+ if (!b.isValid()) {
+ return;
+ }
+
+ // Highlight the receiver
+ gc.useStyle(DrawingStyle.DROP_RECIPIENT);
+ gc.drawRect(b);
+
+ // Get the drop point
+ Point p = (Point) feedback.userData;
+
+ if (p == null) {
+ return;
+ }
+
+ int x = p.x;
+ int y = p.y;
+
+ Rect be = elements[0].getBounds();
+
+ if (be.isValid()) {
+ // At least the first element has a bound. Draw rectangles
+ // for all dropped elements with valid bounds, offset at
+ // the drop point.
+ int offsetX = x - be.x + (feedback.dragBounds != null ? feedback.dragBounds.x : 0);
+ int offsetY = y - be.y + (feedback.dragBounds != null ? feedback.dragBounds.y : 0);
+ gc.useStyle(DrawingStyle.DROP_PREVIEW);
+ for (IDragElement element : elements) {
+ drawElement(gc, element, offsetX, offsetY);
+ }
+ } else {
+ // We don't have bounds for new elements. In this case
+ // just draw cross hairs to the drop point.
+ gc.useStyle(DrawingStyle.GUIDELINE);
+ gc.drawLine(x, b.y, x, b.y + b.h);
+ gc.drawLine(b.x, y, b.x + b.w, y);
+
+ // Use preview lines to indicate the bottom quadrant as well (to
+ // indicate that you are looking at the top left position of the
+ // drop, not the center for example)
+ gc.useStyle(DrawingStyle.DROP_PREVIEW);
+ gc.drawLine(x, y, b.x + b.w, y);
+ gc.drawLine(x, y, x, b.y + b.h);
+ }
+ }
+
+ @Override
+ public DropFeedback onDropMove(@NonNull INode targetNode, @NonNull IDragElement[] elements,
+ @Nullable DropFeedback feedback, @NonNull Point p) {
+ // Update the data used by the DropFeedback.paintCallback above.
+ feedback.userData = p;
+ feedback.requestPaint = true;
+
+ return feedback;
+ }
+
+ @Override
+ public void onDropLeave(@NonNull INode targetNode, @NonNull IDragElement[] elements,
+ @Nullable DropFeedback feedback) {
+ // Nothing to do.
+ }
+
+ @Override
+ public void onDropped(final @NonNull INode targetNode, final @NonNull IDragElement[] elements,
+ final @Nullable DropFeedback feedback, final @NonNull Point p) {
+
+ final Rect b = targetNode.getBounds();
+ if (!b.isValid()) {
+ return;
+ }
+
+ // 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 AbsoluteLayout", new INodeHandler() {
+ @Override
+ public void handle(@NonNull INode node) {
+ boolean first = true;
+ Point offset = null;
+
+ // Now write the new elements.
+ for (IDragElement element : elements) {
+ String fqcn = element.getFqcn();
+ Rect be = element.getBounds();
+
+ INode newChild = targetNode.appendChild(fqcn);
+
+ // Copy all the attributes, modifying them as needed.
+ addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER);
+
+ int deltaX = (feedback.dragBounds != null ? feedback.dragBounds.x : 0);
+ int deltaY = (feedback.dragBounds != null ? feedback.dragBounds.y : 0);
+
+ int x = p.x - b.x + deltaX;
+ int y = p.y - b.y + deltaY;
+
+ if (first) {
+ first = false;
+ if (be.isValid()) {
+ offset = new Point(x - be.x, y - be.y);
+ }
+ } else if (offset != null && be.isValid()) {
+ x = offset.x + be.x;
+ y = offset.y + be.y;
+ } else {
+ x += 10;
+ y += be.isValid() ? be.h : 10;
+ }
+
+ double scale = feedback.dipScale;
+ if (scale != 1.0) {
+ x *= scale;
+ y *= scale;
+ }
+
+ newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_X,
+ String.format(VALUE_N_DP, x));
+ newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_Y,
+ String.format(VALUE_N_DP, y));
+
+ addInnerElements(newChild, element, idMap);
+ }
+ }
+ });
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Overridden in this layout in order to let the top left coordinate be affected by
+ * the resize operation too. In other words, dragging the top left corner to resize a
+ * widget will not only change the size of the widget, it will also move it (though in
+ * this case, the bottom right corner will stay fixed).
+ */
+ @Override
+ 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,
+ mRulesEngine.pxToDp(newBounds.x - node.getParent().getBounds().x)));
+ }
+ if (horizontalEdge != null && newBounds.y != previousBounds.y) {
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_Y,
+ String.format(VALUE_N_DP,
+ mRulesEngine.pxToDp(newBounds.y - node.getParent().getBounds().y)));
+ }
+ }
+
+ @Override
+ protected String getResizeUpdateMessage(ResizeState resizeState, INode child, INode parent,
+ Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) {
+ Rect parentBounds = parent.getBounds();
+ if (horizontalEdge == SegmentType.BOTTOM && verticalEdge == SegmentType.RIGHT) {
+ return super.getResizeUpdateMessage(resizeState, child, parent, newBounds,
+ horizontalEdge, verticalEdge);
+ }
+ return String.format("x=%d, y=%d\nwidth=%s, height=%s",
+ mRulesEngine.pxToDp(newBounds.x - parentBounds.x),
+ mRulesEngine.pxToDp(newBounds.y - parentBounds.y),
+ resizeState.getWidthAttribute(), resizeState.getHeightAttribute());
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AdapterViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AdapterViewRule.java
new file mode 100644
index 000000000..28f5fc95e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AdapterViewRule.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 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;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.DropFeedback;
+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.INode;
+import com.android.ide.common.api.Point;
+import com.android.ide.common.api.Rect;
+
+/** Rule for AdapterView subclasses that don't have more specific rules */
+public class AdapterViewRule extends BaseLayoutRule {
+ @Override
+ public DropFeedback onDropEnter(@NonNull INode targetNode, @Nullable Object targetView,
+ @Nullable IDragElement[] elements) {
+ // You are not allowed to insert children into AdapterViews; you must
+ // use the dedicated addView methods etc dynamically
+ DropFeedback dropFeedback = new DropFeedback(null, new IFeedbackPainter() {
+ @Override
+ public void paint(@NonNull IGraphics gc, @NonNull INode node,
+ @NonNull DropFeedback feedback) {
+ Rect b = node.getBounds();
+ if (b.isValid()) {
+ gc.useStyle(DrawingStyle.DROP_RECIPIENT);
+ gc.drawRect(b);
+ }
+ }
+ });
+ String fqcn = targetNode.getFqcn();
+ String name = fqcn.substring(fqcn.lastIndexOf('.') +1);
+ dropFeedback.errorMessage = String.format(
+ "%s cannot be configured via XML; add content to the AdapterView using Java code",
+ name);
+ dropFeedback.invalidTarget = true;
+ return dropFeedback;
+ }
+
+ @Override
+ public DropFeedback onDropMove(@NonNull INode targetNode, @NonNull IDragElement[] elements,
+ @Nullable DropFeedback feedback, @NonNull Point p) {
+ feedback.invalidTarget = true;
+ return feedback;
+ }
+}
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
new file mode 100644
index 000000000..df2c8f473
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java
@@ -0,0 +1,878 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING;
+import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_IN_PARENT;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
+import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
+import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
+import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_LAYOUT_X;
+import static com.android.SdkConstants.ATTR_LAYOUT_Y;
+import static com.android.SdkConstants.VALUE_FILL_PARENT;
+import static com.android.SdkConstants.VALUE_MATCH_PARENT;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+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.IDragElement.IDragAttribute;
+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.MarginType;
+import com.android.ide.common.api.Point;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.api.RuleAction.ChoiceProvider;
+import com.android.ide.common.api.Segment;
+import com.android.ide.common.api.SegmentType;
+import com.android.utils.Pair;
+
+import java.net.URL;
+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;
+
+/**
+ * A {@link IViewRule} for all layouts.
+ */
+public class BaseLayoutRule extends BaseViewRule {
+ private static final String ACTION_FILL_WIDTH = "_fillW"; //$NON-NLS-1$
+ private static final String ACTION_FILL_HEIGHT = "_fillH"; //$NON-NLS-1$
+ private static final String ACTION_MARGIN = "_margin"; //$NON-NLS-1$
+ private static final URL ICON_MARGINS =
+ BaseLayoutRule.class.getResource("margins.png"); //$NON-NLS-1$
+ private static final URL ICON_GRAVITY =
+ BaseLayoutRule.class.getResource("gravity.png"); //$NON-NLS-1$
+ private static final URL ICON_FILL_WIDTH =
+ BaseLayoutRule.class.getResource("fillwidth.png"); //$NON-NLS-1$
+ private static final URL ICON_FILL_HEIGHT =
+ BaseLayoutRule.class.getResource("fillheight.png"); //$NON-NLS-1$
+
+ // ==== Layout Actions support ====
+
+ // The Margin layout parameters are available for LinearLayout, FrameLayout, RelativeLayout,
+ // and their subclasses.
+ protected final RuleAction createMarginAction(final INode parentNode,
+ final List<? extends INode> children) {
+
+ final List<? extends INode> targets = children == null || children.size() == 0 ?
+ Collections.singletonList(parentNode)
+ : children;
+ final INode first = targets.get(0);
+
+ IMenuCallback actionCallback = new IMenuCallback() {
+ @Override
+ public void action(@NonNull RuleAction action,
+ @NonNull List<? extends INode> selectedNodes,
+ final @Nullable String valueId,
+ final @Nullable Boolean newValue) {
+ parentNode.editXml("Change Margins", new INodeHandler() {
+ @Override
+ public void handle(@NonNull INode n) {
+ String uri = ANDROID_URI;
+ String all = first.getStringAttr(uri, ATTR_LAYOUT_MARGIN);
+ String left = first.getStringAttr(uri, ATTR_LAYOUT_MARGIN_LEFT);
+ String right = first.getStringAttr(uri, ATTR_LAYOUT_MARGIN_RIGHT);
+ String top = first.getStringAttr(uri, ATTR_LAYOUT_MARGIN_TOP);
+ String bottom = first.getStringAttr(uri, ATTR_LAYOUT_MARGIN_BOTTOM);
+ String[] margins = mRulesEngine.displayMarginInput(all, left,
+ right, top, bottom);
+ if (margins != null) {
+ assert margins.length == 5;
+ for (INode child : targets) {
+ child.setAttribute(uri, ATTR_LAYOUT_MARGIN, margins[0]);
+ child.setAttribute(uri, ATTR_LAYOUT_MARGIN_LEFT, margins[1]);
+ child.setAttribute(uri, ATTR_LAYOUT_MARGIN_RIGHT, margins[2]);
+ child.setAttribute(uri, ATTR_LAYOUT_MARGIN_TOP, margins[3]);
+ child.setAttribute(uri, ATTR_LAYOUT_MARGIN_BOTTOM, margins[4]);
+ }
+ }
+ }
+ });
+ }
+ };
+
+ return RuleAction.createAction(ACTION_MARGIN, "Change Margins...", actionCallback,
+ ICON_MARGINS, 40, false);
+ }
+
+ // Both LinearLayout and RelativeLayout have a gravity (but RelativeLayout applies it
+ // to the parent whereas for LinearLayout it's on the children)
+ protected final RuleAction createGravityAction(final List<? extends INode> targets, final
+ String attributeName) {
+ if (targets != null && targets.size() > 0) {
+ final INode first = targets.get(0);
+ ChoiceProvider provider = new ChoiceProvider() {
+ @Override
+ public void addChoices(@NonNull List<String> titles, @NonNull List<URL> iconUrls,
+ @NonNull List<String> ids) {
+ IAttributeInfo info = first.getAttributeInfo(ANDROID_URI, attributeName);
+ if (info != null) {
+ // Generate list of possible gravity value constants
+ assert info.getFormats().contains(IAttributeInfo.Format.FLAG);
+ for (String name : info.getFlagValues()) {
+ titles.add(getAttributeDisplayName(name));
+ ids.add(name);
+ }
+ }
+ }
+ };
+
+ return RuleAction.createChoices("_gravity", "Change Gravity", //$NON-NLS-1$
+ new PropertyCallback(targets, "Change Gravity", ANDROID_URI,
+ attributeName),
+ provider,
+ first.getStringAttr(ANDROID_URI, attributeName), ICON_GRAVITY,
+ 43, false);
+ }
+
+ return null;
+ }
+
+ @Override
+ public void addLayoutActions(
+ @NonNull List<RuleAction> actions,
+ final @NonNull INode parentNode,
+ final @NonNull List<? extends INode> children) {
+ super.addLayoutActions(actions, parentNode, children);
+
+ final List<? extends INode> targets = children == null || children.size() == 0 ?
+ Collections.singletonList(parentNode)
+ : children;
+ final INode first = targets.get(0);
+
+ // Shared action callback
+ IMenuCallback actionCallback = new IMenuCallback() {
+ @Override
+ public void action(
+ @NonNull RuleAction action,
+ @NonNull List<? extends INode> selectedNodes,
+ final @Nullable String valueId,
+ final @Nullable Boolean newValue) {
+ final String actionId = action.getId();
+ final String undoLabel;
+ if (actionId.equals(ACTION_FILL_WIDTH)) {
+ undoLabel = "Change Width Fill";
+ } else if (actionId.equals(ACTION_FILL_HEIGHT)) {
+ undoLabel = "Change Height Fill";
+ } else {
+ return;
+ }
+ parentNode.editXml(undoLabel, new INodeHandler() {
+ @Override
+ public void handle(@NonNull INode n) {
+ String attribute = actionId.equals(ACTION_FILL_WIDTH)
+ ? ATTR_LAYOUT_WIDTH : ATTR_LAYOUT_HEIGHT;
+ String value;
+ if (newValue) {
+ if (supportsMatchParent()) {
+ value = VALUE_MATCH_PARENT;
+ } else {
+ value = VALUE_FILL_PARENT;
+ }
+ } else {
+ value = VALUE_WRAP_CONTENT;
+ }
+ for (INode child : targets) {
+ child.setAttribute(ANDROID_URI, attribute, value);
+ }
+ }
+ });
+ }
+ };
+
+ actions.add(RuleAction.createToggle(ACTION_FILL_WIDTH, "Toggle Fill Width",
+ isFilled(first, ATTR_LAYOUT_WIDTH), actionCallback, ICON_FILL_WIDTH, 10, false));
+ actions.add(RuleAction.createToggle(ACTION_FILL_HEIGHT, "Toggle Fill Height",
+ isFilled(first, ATTR_LAYOUT_HEIGHT), actionCallback, ICON_FILL_HEIGHT, 20, false));
+ }
+
+ // ==== Paste support ====
+
+ /**
+ * The default behavior for pasting in a layout is to simulate a drop in the
+ * top-left corner of the view.
+ * <p/>
+ * Note that we explicitly do not call super() here -- the BaseViewRule.onPaste handler
+ * will call onPasteBeforeChild() instead.
+ * <p/>
+ * Derived layouts should override this behavior if not appropriate.
+ */
+ @Override
+ public void onPaste(@NonNull INode targetNode, @Nullable Object targetView,
+ @NonNull IDragElement[] elements) {
+ DropFeedback feedback = onDropEnter(targetNode, targetView, elements);
+ if (feedback != null) {
+ Point p = targetNode.getBounds().getTopLeft();
+ feedback = onDropMove(targetNode, elements, feedback, p);
+ if (feedback != null) {
+ onDropLeave(targetNode, elements, feedback);
+ onDropped(targetNode, elements, feedback, p);
+ }
+ }
+ }
+
+ /**
+ * The default behavior for pasting in a layout with a specific child target
+ * is to simulate a drop right above the top left of the given child target.
+ * <p/>
+ * This method is invoked by BaseView when onPaste() is called --
+ * views don't generally accept children and instead use the target node as
+ * a hint to paste "before" it.
+ *
+ * @param parentNode the parent node we're pasting into
+ * @param parentView the view object for the parent layout, or null
+ * @param targetNode the first selected node
+ * @param elements the elements being pasted
+ */
+ public void onPasteBeforeChild(INode parentNode, Object parentView, INode targetNode,
+ IDragElement[] elements) {
+ DropFeedback feedback = onDropEnter(parentNode, parentView, elements);
+ if (feedback != null) {
+ Point parentP = parentNode.getBounds().getTopLeft();
+ Point targetP = targetNode.getBounds().getTopLeft();
+ if (parentP.y < targetP.y) {
+ targetP.y -= 1;
+ }
+
+ feedback = onDropMove(parentNode, elements, feedback, targetP);
+ if (feedback != null) {
+ onDropLeave(parentNode, elements, feedback);
+ onDropped(parentNode, elements, feedback, targetP);
+ }
+ }
+ }
+
+ // ==== Utility methods used by derived layouts ====
+
+ /**
+ * Draws the bounds of the given elements and all its children elements in the canvas
+ * with the specified offset.
+ *
+ * @param gc the graphics context
+ * @param element the element to be drawn
+ * @param offsetX a horizontal delta to add to the current bounds of the element when
+ * drawing it
+ * @param offsetY a vertical delta to add to the current bounds of the element when
+ * drawing it
+ */
+ public void drawElement(IGraphics gc, IDragElement element, int offsetX, int offsetY) {
+ Rect b = element.getBounds();
+ if (b.isValid()) {
+ gc.drawRect(b.x + offsetX, b.y + offsetY, b.x + offsetX + b.w, b.y + offsetY + b.h);
+ }
+
+ for (IDragElement inner : element.getInnerElements()) {
+ drawElement(gc, inner, offsetX, offsetY);
+ }
+ }
+
+ /**
+ * Collect all the "android:id" IDs from the dropped elements. When moving
+ * objects within the same canvas, that's all there is to do. However if the
+ * objects are moved to a different canvas or are copied then set
+ * createNewIds to true to find the existing IDs under targetNode and create
+ * a map with new non-conflicting unique IDs as needed. Returns a map String
+ * old-id => tuple (String new-id, String fqcn) where fqcn is the FQCN of
+ * the element.
+ */
+ protected static Map<String, Pair<String, String>> getDropIdMap(INode targetNode,
+ IDragElement[] elements, boolean createNewIds) {
+ Map<String, Pair<String, String>> idMap = new HashMap<String, Pair<String, String>>();
+
+ if (createNewIds) {
+ collectIds(idMap, elements);
+ // Need to remap ids if necessary
+ idMap = remapIds(targetNode, idMap);
+ }
+
+ return idMap;
+ }
+
+ /**
+ * Fills idMap with a map String id => tuple (String id, String fqcn) where
+ * fqcn is the FQCN of the element (in case we want to generate new IDs
+ * based on the element type.)
+ *
+ * @see #getDropIdMap
+ */
+ protected static Map<String, Pair<String, String>> collectIds(
+ Map<String, Pair<String, String>> idMap,
+ IDragElement[] elements) {
+ for (IDragElement element : elements) {
+ IDragAttribute attr = element.getAttribute(ANDROID_URI, ATTR_ID);
+ if (attr != null) {
+ String id = attr.getValue();
+ if (id != null && id.length() > 0) {
+ idMap.put(id, Pair.of(id, element.getFqcn()));
+ }
+ }
+
+ collectIds(idMap, element.getInnerElements());
+ }
+
+ return idMap;
+ }
+
+ /**
+ * Used by #getDropIdMap to find new IDs in case of conflict.
+ */
+ protected static Map<String, Pair<String, String>> remapIds(INode node,
+ Map<String, Pair<String, String>> idMap) {
+ // Visit the document to get a list of existing ids
+ Set<String> existingIdSet = new HashSet<String>();
+ collectExistingIds(node.getRoot(), existingIdSet);
+
+ Map<String, Pair<String, String>> new_map = new HashMap<String, Pair<String, String>>();
+ for (Map.Entry<String, Pair<String, String>> entry : idMap.entrySet()) {
+ String key = entry.getKey();
+ Pair<String, String> value = entry.getValue();
+
+ String id = normalizeId(key);
+
+ if (!existingIdSet.contains(id)) {
+ // Not a conflict. Use as-is.
+ new_map.put(key, value);
+ if (!key.equals(id)) {
+ new_map.put(id, value);
+ }
+ } else {
+ // There is a conflict. Get a new id.
+ String new_id = findNewId(value.getSecond(), existingIdSet);
+ value = Pair.of(new_id, value.getSecond());
+ new_map.put(id, value);
+ new_map.put(id.replaceFirst("@\\+", "@"), value); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ }
+
+ return new_map;
+ }
+
+ /**
+ * Used by #remapIds to find a new ID for a conflicting element.
+ */
+ protected static String findNewId(String fqcn, Set<String> existingIdSet) {
+ // Get the last component of the FQCN (e.g. "android.view.Button" =>
+ // "Button")
+ String name = fqcn.substring(fqcn.lastIndexOf('.') + 1);
+
+ for (int i = 1; i < 1000000; i++) {
+ String id = String.format("@+id/%s%02d", name, i); //$NON-NLS-1$
+ if (!existingIdSet.contains(id)) {
+ existingIdSet.add(id);
+ return id;
+ }
+ }
+
+ // We'll never reach here.
+ return null;
+ }
+
+ /**
+ * Used by #getDropIdMap to find existing IDs recursively.
+ */
+ protected static void collectExistingIds(INode root, Set<String> existingIdSet) {
+ if (root == null) {
+ return;
+ }
+
+ String id = root.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id != null) {
+ id = normalizeId(id);
+
+ if (!existingIdSet.contains(id)) {
+ existingIdSet.add(id);
+ }
+ }
+
+ for (INode child : root.getChildren()) {
+ collectExistingIds(child, existingIdSet);
+ }
+ }
+
+ /**
+ * Transforms @id/name into @+id/name to treat both forms the same way.
+ */
+ protected static String normalizeId(String id) {
+ if (id.indexOf("@+") == -1) { //$NON-NLS-1$
+ id = id.replaceFirst("@", "@+"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ return id;
+ }
+
+ /**
+ * For use by {@link BaseLayoutRule#addAttributes} A filter should return a
+ * valid replacement string.
+ */
+ protected static interface AttributeFilter {
+ String replace(String attributeUri, String attributeName, String attributeValue);
+ }
+
+ private static final String[] EXCLUDED_ATTRIBUTES = new String[] {
+ // Common
+ ATTR_LAYOUT_GRAVITY,
+
+ // from AbsoluteLayout
+ ATTR_LAYOUT_X,
+ ATTR_LAYOUT_Y,
+
+ // from RelativeLayout
+ ATTR_LAYOUT_ABOVE,
+ ATTR_LAYOUT_BELOW,
+ ATTR_LAYOUT_TO_LEFT_OF,
+ ATTR_LAYOUT_TO_RIGHT_OF,
+ ATTR_LAYOUT_ALIGN_BASELINE,
+ ATTR_LAYOUT_ALIGN_TOP,
+ ATTR_LAYOUT_ALIGN_BOTTOM,
+ ATTR_LAYOUT_ALIGN_LEFT,
+ ATTR_LAYOUT_ALIGN_RIGHT,
+ ATTR_LAYOUT_ALIGN_PARENT_TOP,
+ ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
+ ATTR_LAYOUT_ALIGN_PARENT_LEFT,
+ ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
+ ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING,
+ ATTR_LAYOUT_CENTER_HORIZONTAL,
+ ATTR_LAYOUT_CENTER_IN_PARENT,
+ ATTR_LAYOUT_CENTER_VERTICAL,
+
+ // From GridLayout
+ ATTR_LAYOUT_ROW,
+ ATTR_LAYOUT_ROW_SPAN,
+ ATTR_LAYOUT_COLUMN,
+ ATTR_LAYOUT_COLUMN_SPAN
+ };
+
+ /**
+ * Default attribute filter used by the various layouts to filter out some properties
+ * we don't want to offer.
+ */
+ public static final AttributeFilter DEFAULT_ATTR_FILTER = new AttributeFilter() {
+ Set<String> mExcludes;
+
+ @Override
+ public String replace(String uri, String name, String value) {
+ if (!ANDROID_URI.equals(uri)) {
+ return value;
+ }
+
+ if (mExcludes == null) {
+ mExcludes = new HashSet<String>(EXCLUDED_ATTRIBUTES.length);
+ mExcludes.addAll(Arrays.asList(EXCLUDED_ATTRIBUTES));
+ }
+
+ return mExcludes.contains(name) ? null : value;
+ }
+ };
+
+ /**
+ * Copies all the attributes from oldElement to newNode. Uses the idMap to
+ * transform the value of all attributes of Format.REFERENCE. If filter is
+ * non-null, it's a filter that can rewrite the attribute string.
+ */
+ protected static void addAttributes(INode newNode, IDragElement oldElement,
+ Map<String, Pair<String, String>> idMap, AttributeFilter filter) {
+
+ for (IDragAttribute attr : oldElement.getAttributes()) {
+ String uri = attr.getUri();
+ String name = attr.getName();
+ String value = attr.getValue();
+
+ IAttributeInfo attrInfo = newNode.getAttributeInfo(uri, name);
+ if (attrInfo != null) {
+ if (attrInfo.getFormats().contains(IAttributeInfo.Format.REFERENCE)) {
+ if (idMap.containsKey(value)) {
+ value = idMap.get(value).getFirst();
+ }
+ }
+ }
+
+ if (filter != null) {
+ value = filter.replace(uri, name, value);
+ }
+ if (value != null && value.length() > 0) {
+ newNode.setAttribute(uri, name, value);
+ }
+ }
+ }
+
+ /**
+ * Adds all the children elements of oldElement to newNode, recursively.
+ * Attributes are adjusted by calling addAttributes with idMap as necessary,
+ * with no closure filter.
+ */
+ protected static void addInnerElements(INode newNode, IDragElement oldElement,
+ Map<String, Pair<String, String>> idMap) {
+
+ for (IDragElement element : oldElement.getInnerElements()) {
+ String fqcn = element.getFqcn();
+ INode childNode = newNode.appendChild(fqcn);
+
+ addAttributes(childNode, element, idMap, null /* filter */);
+ addInnerElements(childNode, element, idMap);
+ }
+ }
+
+ /**
+ * Insert the given elements into the given node at the given position
+ *
+ * @param targetNode the node to insert into
+ * @param elements the elements to insert
+ * @param createNewIds if true, generate new ids when there is a conflict
+ * @param initialInsertPos index among targetnode's children which to insert the
+ * children
+ */
+ public static void insertAt(final INode targetNode, final IDragElement[] elements,
+ final boolean createNewIds, final int initialInsertPos) {
+
+ // 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,
+ createNewIds);
+
+ targetNode.editXml("Insert Elements", new INodeHandler() {
+
+ @Override
+ public void handle(@NonNull INode node) {
+ // Now write the new elements.
+ int insertPos = initialInsertPos;
+ for (IDragElement element : elements) {
+ String fqcn = element.getFqcn();
+
+ INode newChild = targetNode.insertChildAt(fqcn, insertPos);
+
+ // insertPos==-1 means to insert at the end. Otherwise
+ // increment the insertion position.
+ if (insertPos >= 0) {
+ insertPos++;
+ }
+
+ // Copy all the attributes, modifying them as needed.
+ addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER);
+ addInnerElements(newChild, element, idMap);
+ }
+ }
+ });
+ }
+
+ // ---- Resizing ----
+
+ /** Creates a new {@link ResizeState} object to track resize state */
+ protected ResizeState createResizeState(INode layout, Object layoutView, INode node) {
+ return new ResizeState(this, layout, layoutView, node);
+ }
+
+ @Override
+ public DropFeedback onResizeBegin(@NonNull INode child, @NonNull INode parent,
+ @Nullable SegmentType horizontalEdge, @Nullable SegmentType verticalEdge,
+ @Nullable Object childView, @Nullable Object parentView) {
+ ResizeState state = createResizeState(parent, parentView, child);
+ 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() {
+ @Override
+ public String getAttribute(@NonNull INode node, @Nullable String namespace,
+ @NonNull 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() {
+ @Override
+ public void paint(@NonNull IGraphics gc, @NonNull INode node,
+ @NonNull DropFeedback feedback) {
+ ResizeState resizeState = (ResizeState) feedback.userData;
+ if (resizeState != null && resizeState.bounds != null) {
+ paintResizeFeedback(gc, node, resizeState);
+ }
+ }
+ });
+ }
+
+ protected void paintResizeFeedback(IGraphics gc, INode node, ResizeState resizeState) {
+ gc.useStyle(DrawingStyle.RESIZE_PREVIEW);
+ Rect b = resizeState.bounds;
+ gc.drawRect(b);
+
+ if (resizeState.horizontalFillSegment != null) {
+ gc.useStyle(DrawingStyle.GUIDELINE);
+ Segment s = resizeState.horizontalFillSegment;
+ gc.drawLine(s.from, s.at, s.to, s.at);
+ }
+ if (resizeState.verticalFillSegment != null) {
+ gc.useStyle(DrawingStyle.GUIDELINE);
+ Segment s = resizeState.verticalFillSegment;
+ gc.drawLine(s.at, s.from, s.at, s.to);
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the maximum number of pixels will be considered a "match" when snapping
+ * resize or move positions to edges or other constraints
+ *
+ * @return the maximum number of pixels to consider for snapping
+ */
+ public static final int getMaxMatchDistance() {
+ // TODO - make constant once we're happy with the feel
+ return 20;
+ }
+
+ @Override
+ public void onResizeUpdate(@Nullable DropFeedback feedback, @NonNull INode child,
+ @NonNull INode parent, @NonNull Rect newBounds, int modifierMask) {
+ ResizeState state = (ResizeState) feedback.userData;
+ state.bounds = newBounds;
+ state.modifierMask = modifierMask;
+
+ // 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;
+ }
+ }
+ }
+
+ // Match on fill bounds
+ state.horizontalFillSegment = null;
+ state.fillHeight = false;
+ if (state.horizontalEdgeType == SegmentType.BOTTOM && !state.wrapHeight) {
+ Rect parentBounds = parent.getBounds();
+ state.horizontalFillSegment = new Segment(parentBounds.y2(), newBounds.x,
+ newBounds.x2(),
+ null /*node*/, null /*id*/, SegmentType.BOTTOM, MarginType.NO_MARGIN);
+ if (Math.abs(newBounds.y2() - parentBounds.y2()) < getMaxMatchDistance()) {
+ state.fillHeight = true;
+ newBounds.h = parentBounds.y2() - newBounds.y;
+ }
+ }
+ state.verticalFillSegment = null;
+ state.fillWidth = false;
+ if (state.verticalEdgeType == SegmentType.RIGHT && !state.wrapWidth) {
+ Rect parentBounds = parent.getBounds();
+ state.verticalFillSegment = new Segment(parentBounds.x2(), newBounds.y,
+ newBounds.y2(),
+ null /*node*/, null /*id*/, SegmentType.RIGHT, MarginType.NO_MARGIN);
+ if (Math.abs(newBounds.x2() - parentBounds.x2()) < getMaxMatchDistance()) {
+ state.fillWidth = true;
+ newBounds.w = parentBounds.x2() - newBounds.x;
+ }
+ }
+
+ feedback.tooltip = getResizeUpdateMessage(state, child, parent,
+ newBounds, state.horizontalEdgeType, state.verticalEdgeType);
+ }
+
+ @Override
+ public void onResizeEnd(@Nullable DropFeedback feedback, @NonNull INode child,
+ final @NonNull INode parent, final @NonNull 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() {
+ @Override
+ public void handle(@NonNull 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.getWidthAttribute();
+ String height = resizeState.getHeightAttribute();
+
+ if (horizontalEdge == null) {
+ return width;
+ } else if (verticalEdge == null) {
+ return height;
+ } else {
+ // U+00D7: Unicode for multiplication sign
+ return String.format("%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 || resizeState.fillWidth)) {
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, resizeState.getWidthAttribute());
+ }
+ if (horizontalEdge != null
+ && (newBounds.h != oldBounds.h || resizeState.wrapHeight || resizeState.fillHeight)) {
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, resizeState.getHeightAttribute());
+ }
+ }
+}
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
new file mode 100644
index 000000000..83ce9ef8f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java
@@ -0,0 +1,996 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_CLASS;
+import static com.android.SdkConstants.ATTR_HINT;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_STYLE;
+import static com.android.SdkConstants.ATTR_TEXT;
+import static com.android.SdkConstants.DOT_LAYOUT_PARAMS;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.VALUE_FALSE;
+import static com.android.SdkConstants.VALUE_FILL_PARENT;
+import static com.android.SdkConstants.VALUE_MATCH_PARENT;
+import static com.android.SdkConstants.VALUE_TRUE;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.AbstractViewRule;
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.api.IClientRulesEngine;
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.IMenuCallback;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewMetadata;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.api.RuleAction.ActionProvider;
+import com.android.ide.common.api.RuleAction.ChoiceProvider;
+import com.android.resources.ResourceType;
+import com.android.utils.Pair;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * Common IViewRule processing to all view and layout classes.
+ */
+public class BaseViewRule extends AbstractViewRule {
+ /** List of recently edited properties */
+ private static List<String> sRecent = new LinkedList<String>();
+
+ /** Maximum number of recent properties to track and list */
+ private final static int MAX_RECENT_COUNT = 12;
+
+ // Strings used as internal ids, group ids and prefixes for actions
+ private static final String FALSE_ID = "false"; //$NON-NLS-1$
+ private static final String TRUE_ID = "true"; //$NON-NLS-1$
+ private static final String PROP_PREFIX = "@prop@"; //$NON-NLS-1$
+ private static final String CLEAR_ID = "clear"; //$NON-NLS-1$
+ private static final String ZCUSTOM = "zcustom"; //$NON-NLS-1$
+
+ protected IClientRulesEngine mRulesEngine;
+
+ // Cache of attributes. Key is FQCN of a node mixed with its view hierarchy
+ // parent. Values are a custom map as needed by getContextMenu.
+ private Map<String, Map<String, Prop>> mAttributesMap =
+ new HashMap<String, Map<String, Prop>>();
+
+ @Override
+ public boolean onInitialize(@NonNull String fqcn, @NonNull IClientRulesEngine engine) {
+ mRulesEngine = engine;
+
+ // This base rule can handle any class so we don't need to filter on
+ // FQCN. Derived classes should do so if they can handle some
+ // subclasses.
+
+ // If onInitialize returns false, it means it can't handle the given
+ // FQCN and will be unloaded.
+
+ return true;
+ }
+
+ /**
+ * Returns the {@link IClientRulesEngine} associated with this {@link IViewRule}
+ *
+ * @return the {@link IClientRulesEngine} associated with this {@link IViewRule}
+ */
+ public IClientRulesEngine getRulesEngine() {
+ return mRulesEngine;
+ }
+
+ // === Context Menu ===
+
+ /**
+ * Generate custom actions for the context menu: <br/>
+ * - Explicit layout_width and layout_height attributes.
+ * - List of all other simple toggle attributes.
+ */
+ @Override
+ public void addContextMenuActions(@NonNull List<RuleAction> actions,
+ final @NonNull INode selectedNode) {
+ String width = null;
+ String currentWidth = selectedNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WIDTH);
+
+ String fillParent = getFillParentValueName();
+ boolean canMatchParent = supportsMatchParent();
+ if (canMatchParent && VALUE_FILL_PARENT.equals(currentWidth)) {
+ currentWidth = VALUE_MATCH_PARENT;
+ } else if (!canMatchParent && VALUE_MATCH_PARENT.equals(currentWidth)) {
+ currentWidth = VALUE_FILL_PARENT;
+ } else if (!VALUE_WRAP_CONTENT.equals(currentWidth) && !fillParent.equals(currentWidth)) {
+ width = currentWidth;
+ }
+
+ String height = null;
+ String currentHeight = selectedNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
+
+ if (canMatchParent && VALUE_FILL_PARENT.equals(currentHeight)) {
+ currentHeight = VALUE_MATCH_PARENT;
+ } else if (!canMatchParent && VALUE_MATCH_PARENT.equals(currentHeight)) {
+ currentHeight = VALUE_FILL_PARENT;
+ } else if (!VALUE_WRAP_CONTENT.equals(currentHeight)
+ && !fillParent.equals(currentHeight)) {
+ height = currentHeight;
+ }
+ final String newWidth = width;
+ final String newHeight = height;
+
+ final IMenuCallback onChange = new IMenuCallback() {
+ @Override
+ public void action(
+ final @NonNull RuleAction action,
+ final @NonNull List<? extends INode> selectedNodes,
+ final @Nullable String valueId, final @Nullable Boolean newValue) {
+ String fullActionId = action.getId();
+ boolean isProp = fullActionId.startsWith(PROP_PREFIX);
+ final String actionId = isProp ?
+ fullActionId.substring(PROP_PREFIX.length()) : fullActionId;
+
+ if (fullActionId.equals(ATTR_LAYOUT_WIDTH)) {
+ final String newAttrValue = getValue(valueId, newWidth);
+ if (newAttrValue != null) {
+ for (INode node : selectedNodes) {
+ node.editXml("Change Attribute " + ATTR_LAYOUT_WIDTH,
+ new PropertySettingNodeHandler(ANDROID_URI,
+ ATTR_LAYOUT_WIDTH, newAttrValue));
+ }
+ editedProperty(ATTR_LAYOUT_WIDTH);
+ }
+ return;
+ } else if (fullActionId.equals(ATTR_LAYOUT_HEIGHT)) {
+ // Ask the user
+ final String newAttrValue = getValue(valueId, newHeight);
+ if (newAttrValue != null) {
+ for (INode node : selectedNodes) {
+ node.editXml("Change Attribute " + ATTR_LAYOUT_HEIGHT,
+ new PropertySettingNodeHandler(ANDROID_URI,
+ ATTR_LAYOUT_HEIGHT, newAttrValue));
+ }
+ editedProperty(ATTR_LAYOUT_HEIGHT);
+ }
+ return;
+ } else if (fullActionId.equals(ATTR_ID)) {
+ // Ids must be set individually so open the id dialog for each
+ // selected node (though allow cancel to break the loop)
+ for (INode node : selectedNodes) {
+ if (!mRulesEngine.rename(node)) {
+ break;
+ }
+ }
+ editedProperty(ATTR_ID);
+ return;
+ } else if (isProp) {
+ INode firstNode = selectedNodes.get(0);
+ String key = getPropertyMapKey(selectedNode);
+ Map<String, Prop> props = mAttributesMap.get(key);
+ final Prop prop = (props != null) ? props.get(actionId) : null;
+
+ if (prop != null) {
+ editedProperty(actionId);
+
+ // For custom values (requiring an input dialog) input the
+ // value outside the undo-block.
+ // Input the value as a text, unless we know it's the "text" or
+ // "style" attributes (where we know we want to ask for specific
+ // resource types).
+ String uri = ANDROID_URI;
+ String v = null;
+ if (prop.isStringEdit()) {
+ boolean isStyle = actionId.equals(ATTR_STYLE);
+ boolean isText = actionId.equals(ATTR_TEXT);
+ boolean isHint = actionId.equals(ATTR_HINT);
+ if (isStyle || isText || isHint) {
+ String resourceTypeName = isStyle
+ ? ResourceType.STYLE.getName()
+ : ResourceType.STRING.getName();
+ String oldValue = selectedNodes.size() == 1
+ ? (isStyle ? firstNode.getStringAttr(ATTR_STYLE, actionId)
+ : firstNode.getStringAttr(ANDROID_URI, actionId))
+ : ""; //$NON-NLS-1$
+ oldValue = ensureValidString(oldValue);
+ v = mRulesEngine.displayResourceInput(resourceTypeName, oldValue);
+ if (isStyle) {
+ uri = null;
+ }
+ } else if (actionId.equals(ATTR_CLASS) && selectedNodes.size() >= 1 &&
+ VIEW_FRAGMENT.equals(selectedNodes.get(0).getFqcn())) {
+ v = mRulesEngine.displayFragmentSourceInput();
+ uri = null;
+ } else {
+ v = inputAttributeValue(firstNode, actionId);
+ }
+ }
+ final String customValue = v;
+
+ for (INode n : selectedNodes) {
+ if (prop.isToggle()) {
+ // case of toggle
+ String value = ""; //$NON-NLS-1$
+ if (valueId.equals(TRUE_ID)) {
+ value = newValue ? "true" : ""; //$NON-NLS-1$ //$NON-NLS-2$
+ } else if (valueId.equals(FALSE_ID)) {
+ value = newValue ? "false" : "";//$NON-NLS-1$ //$NON-NLS-2$
+ }
+ n.setAttribute(uri, actionId, value);
+ } else if (prop.isFlag()) {
+ // case of a flag
+ String values = ""; //$NON-NLS-1$
+ if (!valueId.equals(CLEAR_ID)) {
+ values = n.getStringAttr(ANDROID_URI, actionId);
+ Set<String> newValues = new HashSet<String>();
+ if (values != null) {
+ newValues.addAll(Arrays.asList(
+ values.split("\\|"))); //$NON-NLS-1$
+ }
+ if (newValue) {
+ newValues.add(valueId);
+ } else {
+ newValues.remove(valueId);
+ }
+
+ List<String> sorted = new ArrayList<String>(newValues);
+ Collections.sort(sorted);
+ values = join('|', sorted);
+
+ // Special case
+ if (valueId.equals("normal")) { //$NON-NLS-1$
+ // For textStyle for example, if you have "bold|italic"
+ // and you select the "normal" property, this should
+ // not behave in the normal flag way and "or" itself in;
+ // it should replace the other two.
+ // This also applies to imeOptions.
+ values = valueId;
+ }
+ }
+ n.setAttribute(uri, actionId, values);
+ } else if (prop.isEnum()) {
+ // case of an enum
+ String value = ""; //$NON-NLS-1$
+ if (!valueId.equals(CLEAR_ID)) {
+ value = newValue ? valueId : ""; //$NON-NLS-1$
+ }
+ n.setAttribute(uri, actionId, value);
+ } else {
+ assert prop.isStringEdit();
+ // We've already received the value outside the undo block
+ if (customValue != null) {
+ n.setAttribute(uri, actionId, customValue);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Input the custom value for the given attribute. This will use the Reference
+ * Chooser if it is a reference value, otherwise a plain text editor.
+ */
+ private String inputAttributeValue(final INode node, final String attribute) {
+ String oldValue = node.getStringAttr(ANDROID_URI, attribute);
+ oldValue = ensureValidString(oldValue);
+ IAttributeInfo attributeInfo = node.getAttributeInfo(ANDROID_URI, attribute);
+ if (attributeInfo != null
+ && attributeInfo.getFormats().contains(Format.REFERENCE)) {
+ return mRulesEngine.displayReferenceInput(oldValue);
+ } else {
+ // A single resource type? If so use a resource chooser initialized
+ // to this specific type
+ /* This does not work well, because the metadata is a bit misleading:
+ * for example a Button's "text" property and a Button's "onClick" property
+ * both claim to be of type [string], but @string/ is NOT valid for
+ * onClick..
+ if (attributeInfo != null && attributeInfo.getFormats().length == 1) {
+ // Resource chooser
+ Format format = attributeInfo.getFormats()[0];
+ return mRulesEngine.displayResourceInput(format.name(), oldValue);
+ }
+ */
+
+ // Fallback: just edit the raw XML string
+ String message = String.format("New %1$s Value:", attribute);
+ return mRulesEngine.displayInput(message, oldValue, null);
+ }
+ }
+
+ /**
+ * Returns the value (which will ask the user if the value is the special
+ * {@link #ZCUSTOM} marker
+ */
+ private String getValue(String valueId, String defaultValue) {
+ if (valueId.equals(ZCUSTOM)) {
+ if (defaultValue == null) {
+ defaultValue = "";
+ }
+ String value = mRulesEngine.displayInput(
+ "Set custom layout attribute value (example: 50dp)",
+ defaultValue, null);
+ if (value != null && value.trim().length() > 0) {
+ return value.trim();
+ } else {
+ return null;
+ }
+ }
+
+ return valueId;
+ }
+ };
+
+ IAttributeInfo textAttribute = selectedNode.getAttributeInfo(ANDROID_URI, ATTR_TEXT);
+ if (textAttribute != null) {
+ actions.add(RuleAction.createAction(PROP_PREFIX + ATTR_TEXT, "Edit Text...", onChange,
+ null, 10, true));
+ }
+
+ String editIdLabel = selectedNode.getStringAttr(ANDROID_URI, ATTR_ID) != null ?
+ "Edit ID..." : "Assign ID...";
+ actions.add(RuleAction.createAction(ATTR_ID, editIdLabel, onChange, null, 20, true));
+
+ addCommonPropertyActions(actions, selectedNode, onChange, 21);
+
+ // Create width choice submenu
+ actions.add(RuleAction.createSeparator(32));
+ List<Pair<String, String>> widthChoices = new ArrayList<Pair<String,String>>(4);
+ widthChoices.add(Pair.of(VALUE_WRAP_CONTENT, "Wrap Content"));
+ if (canMatchParent) {
+ widthChoices.add(Pair.of(VALUE_MATCH_PARENT, "Match Parent"));
+ } else {
+ widthChoices.add(Pair.of(VALUE_FILL_PARENT, "Fill Parent"));
+ }
+ if (width != null) {
+ widthChoices.add(Pair.of(width, width));
+ }
+ widthChoices.add(Pair.of(ZCUSTOM, "Other..."));
+ actions.add(RuleAction.createChoices(
+ ATTR_LAYOUT_WIDTH, "Layout Width",
+ onChange,
+ null /* iconUrls */,
+ currentWidth,
+ null, 35,
+ true, // supportsMultipleNodes
+ widthChoices));
+
+ // Create height choice submenu
+ List<Pair<String, String>> heightChoices = new ArrayList<Pair<String,String>>(4);
+ heightChoices.add(Pair.of(VALUE_WRAP_CONTENT, "Wrap Content"));
+ if (canMatchParent) {
+ heightChoices.add(Pair.of(VALUE_MATCH_PARENT, "Match Parent"));
+ } else {
+ heightChoices.add(Pair.of(VALUE_FILL_PARENT, "Fill Parent"));
+ }
+ if (height != null) {
+ heightChoices.add(Pair.of(height, height));
+ }
+ heightChoices.add(Pair.of(ZCUSTOM, "Other..."));
+ actions.add(RuleAction.createChoices(
+ ATTR_LAYOUT_HEIGHT, "Layout Height",
+ onChange,
+ null /* iconUrls */,
+ currentHeight,
+ null, 40,
+ true,
+ heightChoices));
+
+ actions.add(RuleAction.createSeparator(45));
+ RuleAction properties = RuleAction.createChoices("properties", "Other Properties", //$NON-NLS-1$
+ onChange /*callback*/, null /*icon*/, 50,
+ true /*supportsMultipleNodes*/, new ActionProvider() {
+ @Override
+ public @NonNull List<RuleAction> getNestedActions(@NonNull INode node) {
+ List<RuleAction> propertyActionTypes = new ArrayList<RuleAction>();
+ propertyActionTypes.add(RuleAction.createChoices(
+ "recent", "Recent", //$NON-NLS-1$
+ onChange /*callback*/, null /*icon*/, 10,
+ true /*supportsMultipleNodes*/, new ActionProvider() {
+ @Override
+ public @NonNull List<RuleAction> getNestedActions(@NonNull INode n) {
+ List<RuleAction> propertyActions = new ArrayList<RuleAction>();
+ addRecentPropertyActions(propertyActions, n, onChange);
+ return propertyActions;
+ }
+ }));
+
+ propertyActionTypes.add(RuleAction.createSeparator(20));
+
+ addInheritedProperties(propertyActionTypes, node, onChange, 30);
+
+ propertyActionTypes.add(RuleAction.createSeparator(50));
+ propertyActionTypes.add(RuleAction.createChoices(
+ "layoutparams", "Layout Parameters", //$NON-NLS-1$
+ onChange /*callback*/, null /*icon*/, 60,
+ true /*supportsMultipleNodes*/, new ActionProvider() {
+ @Override
+ public @NonNull List<RuleAction> getNestedActions(@NonNull INode n) {
+ List<RuleAction> propertyActions = new ArrayList<RuleAction>();
+ addPropertyActions(propertyActions, n, onChange, null, true);
+ return propertyActions;
+ }
+ }));
+
+ propertyActionTypes.add(RuleAction.createSeparator(70));
+
+ propertyActionTypes.add(RuleAction.createChoices(
+ "allprops", "All By Name", //$NON-NLS-1$
+ onChange /*callback*/, null /*icon*/, 80,
+ true /*supportsMultipleNodes*/, new ActionProvider() {
+ @Override
+ public @NonNull List<RuleAction> getNestedActions(@NonNull INode n) {
+ List<RuleAction> propertyActions = new ArrayList<RuleAction>();
+ addPropertyActions(propertyActions, n, onChange, null, false);
+ return propertyActions;
+ }
+ }));
+
+ return propertyActionTypes;
+ }
+ });
+
+ actions.add(properties);
+ }
+
+ @Override
+ @Nullable
+ public String getDefaultActionId(@NonNull final INode selectedNode) {
+ IAttributeInfo textAttribute = selectedNode.getAttributeInfo(ANDROID_URI, ATTR_TEXT);
+ if (textAttribute != null) {
+ return PROP_PREFIX + ATTR_TEXT;
+ }
+
+ return null;
+ }
+
+ private static String getPropertyMapKey(INode node) {
+ // Compute the key for mAttributesMap. This depends on the type of this
+ // node and its parent in the view hierarchy.
+ StringBuilder sb = new StringBuilder();
+ sb.append(node.getFqcn());
+ sb.append('_');
+ INode parent = node.getParent();
+ if (parent != null) {
+ sb.append(parent.getFqcn());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Adds menu items for the inherited attributes, one pull-right menu for each super class
+ * that defines attributes.
+ *
+ * @param propertyActionTypes the actions list to add into
+ * @param node the node to apply the attributes to
+ * @param onChange the callback to use for setting attributes
+ * @param sortPriority the initial sort attribute for the first menu item
+ */
+ private void addInheritedProperties(List<RuleAction> propertyActionTypes, INode node,
+ final IMenuCallback onChange, int sortPriority) {
+ List<String> attributeSources = node.getAttributeSources();
+ for (final String definedBy : attributeSources) {
+ String sourceClass = definedBy;
+
+ // Strip package prefixes when necessary
+ int index = sourceClass.length();
+ if (sourceClass.endsWith(DOT_LAYOUT_PARAMS)) {
+ index = sourceClass.length() - DOT_LAYOUT_PARAMS.length() - 1;
+ }
+ int lastDot = sourceClass.lastIndexOf('.', index);
+ if (lastDot != -1) {
+ sourceClass = sourceClass.substring(lastDot + 1);
+ }
+
+ String label;
+ if (definedBy.equals(node.getFqcn())) {
+ label = String.format("Defined by %1$s", sourceClass);
+ } else {
+ label = String.format("Inherited from %1$s", sourceClass);
+ }
+
+ propertyActionTypes.add(RuleAction.createChoices("def_" + definedBy,
+ label,
+ onChange /*callback*/, null /*icon*/, sortPriority++,
+ true /*supportsMultipleNodes*/, new ActionProvider() {
+ @Override
+ public @NonNull List<RuleAction> getNestedActions(@NonNull INode n) {
+ List<RuleAction> propertyActions = new ArrayList<RuleAction>();
+ addPropertyActions(propertyActions, n, onChange, definedBy, false);
+ return propertyActions;
+ }
+ }));
+ }
+ }
+
+ /**
+ * Creates a list of properties that are commonly edited for views of the
+ * selected node's type
+ */
+ private void addCommonPropertyActions(List<RuleAction> actions, INode selectedNode,
+ IMenuCallback onChange, int sortPriority) {
+ Map<String, Prop> properties = getPropertyMetadata(selectedNode);
+ IViewMetadata metadata = mRulesEngine.getMetadata(selectedNode.getFqcn());
+ if (metadata != null) {
+ List<String> attributes = metadata.getTopAttributes();
+ if (attributes.size() > 0) {
+ for (String attribute : attributes) {
+ // Text and ID are handled manually in the menu construction code because
+ // we want to place them consistently and customize the action label
+ if (ATTR_TEXT.equals(attribute) || ATTR_ID.equals(attribute)) {
+ continue;
+ }
+
+ Prop property = properties.get(attribute);
+ if (property != null) {
+ String title = property.getTitle();
+ if (title.endsWith("...")) {
+ title = String.format("Edit %1$s", property.getTitle());
+ }
+ actions.add(createPropertyAction(property, attribute, title,
+ selectedNode, onChange, sortPriority));
+ sortPriority++;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Record that the given property was just edited; adds it to the front of
+ * the recently edited property list
+ *
+ * @param property the name of the property
+ */
+ static void editedProperty(String property) {
+ if (sRecent.contains(property)) {
+ sRecent.remove(property);
+ } else if (sRecent.size() > MAX_RECENT_COUNT) {
+ sRecent.remove(sRecent.size() - 1);
+ }
+ sRecent.add(0, property);
+ }
+
+ /**
+ * Creates a list of recently modified properties that apply to the given selected node
+ */
+ private void addRecentPropertyActions(List<RuleAction> actions, INode selectedNode,
+ IMenuCallback onChange) {
+ int sortPriority = 10;
+ Map<String, Prop> properties = getPropertyMetadata(selectedNode);
+ for (String attribute : sRecent) {
+ Prop property = properties.get(attribute);
+ if (property != null) {
+ actions.add(createPropertyAction(property, attribute, property.getTitle(),
+ selectedNode, onChange, sortPriority));
+ sortPriority += 10;
+ }
+ }
+ }
+
+ /**
+ * Creates a list of nested actions representing the property-setting
+ * actions for the given selected node
+ */
+ private void addPropertyActions(List<RuleAction> actions, INode selectedNode,
+ IMenuCallback onChange, String definedBy, boolean layoutParamsOnly) {
+
+ Map<String, Prop> properties = getPropertyMetadata(selectedNode);
+
+ int sortPriority = 10;
+ for (Map.Entry<String, Prop> entry : properties.entrySet()) {
+ String id = entry.getKey();
+ Prop property = entry.getValue();
+ if (layoutParamsOnly) {
+ // If we have definedBy information, that is most accurate; all layout
+ // params will be defined by a class whose name ends with
+ // .LayoutParams:
+ if (definedBy != null) {
+ if (!definedBy.endsWith(DOT_LAYOUT_PARAMS)) {
+ continue;
+ }
+ } else if (!id.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
+ continue;
+ }
+ }
+ if (definedBy != null && !definedBy.equals(property.getDefinedBy())) {
+ continue;
+ }
+ actions.add(createPropertyAction(property, id, property.getTitle(),
+ selectedNode, onChange, sortPriority));
+ sortPriority += 10;
+ }
+
+ // The properties are coming out of map key order which isn't right, so sort
+ // alphabetically instead
+ Collections.sort(actions, new Comparator<RuleAction>() {
+ @Override
+ public int compare(RuleAction action1, RuleAction action2) {
+ return action1.getTitle().compareTo(action2.getTitle());
+ }
+ });
+ }
+
+ private RuleAction createPropertyAction(Prop p, String id, String title, INode selectedNode,
+ IMenuCallback onChange, int sortPriority) {
+ if (p.isToggle()) {
+ // Toggles are handled as a multiple-choice between true, false
+ // and nothing (clear)
+ String value = selectedNode.getStringAttr(ANDROID_URI, id);
+ if (value != null) {
+ value = value.toLowerCase(Locale.US);
+ }
+ if (VALUE_TRUE.equals(value)) {
+ value = TRUE_ID;
+ } else if (VALUE_FALSE.equals(value)) {
+ value = FALSE_ID;
+ } else {
+ value = CLEAR_ID;
+ }
+ return RuleAction.createChoices(PROP_PREFIX + id, title,
+ onChange, BOOLEAN_CHOICE_PROVIDER,
+ value,
+ null, sortPriority,
+ true);
+ } else if (p.getChoices() != null) {
+ // Enum or flags. Their possible values are the multiple-choice
+ // items, with an extra "clear" option to remove everything.
+ String current = selectedNode.getStringAttr(ANDROID_URI, id);
+ if (current == null || current.length() == 0) {
+ current = CLEAR_ID;
+ }
+ return RuleAction.createChoices(PROP_PREFIX + id, title,
+ onChange, new EnumPropertyChoiceProvider(p),
+ current,
+ null, sortPriority,
+ true);
+ } else {
+ return RuleAction.createAction(
+ PROP_PREFIX + id,
+ title,
+ onChange,
+ null, sortPriority,
+ true);
+ }
+ }
+
+ private Map<String, Prop> getPropertyMetadata(final INode selectedNode) {
+ String key = getPropertyMapKey(selectedNode);
+ Map<String, Prop> props = mAttributesMap.get(key);
+ if (props == null) {
+ // Prepare the property map
+ props = new HashMap<String, Prop>();
+ for (IAttributeInfo attrInfo : selectedNode.getDeclaredAttributes()) {
+ String id = attrInfo != null ? attrInfo.getName() : null;
+ if (id == null || id.equals(ATTR_LAYOUT_WIDTH) || id.equals(ATTR_LAYOUT_HEIGHT)) {
+ // Layout width/height are already handled at the root level
+ continue;
+ }
+ if (attrInfo == null) {
+ continue;
+ }
+ EnumSet<Format> formats = attrInfo.getFormats();
+
+ String title = getAttributeDisplayName(id);
+
+ String definedBy = attrInfo != null ? attrInfo.getDefinedBy() : null;
+ if (formats.contains(IAttributeInfo.Format.BOOLEAN)) {
+ props.put(id, new Prop(title, true, definedBy));
+ } else if (formats.contains(IAttributeInfo.Format.ENUM)) {
+ // Convert each enum into a map id=>title
+ Map<String, String> values = new HashMap<String, String>();
+ if (attrInfo != null) {
+ for (String e : attrInfo.getEnumValues()) {
+ values.put(e, getAttributeDisplayName(e));
+ }
+ }
+
+ props.put(id, new Prop(title, false, false, values, definedBy));
+ } else if (formats.contains(IAttributeInfo.Format.FLAG)) {
+ // Convert each flag into a map id=>title
+ Map<String, String> values = new HashMap<String, String>();
+ if (attrInfo != null) {
+ for (String e : attrInfo.getFlagValues()) {
+ values.put(e, getAttributeDisplayName(e));
+ }
+ }
+
+ props.put(id, new Prop(title, false, true, values, definedBy));
+ } else {
+ props.put(id, new Prop(title + "...", false, definedBy));
+ }
+ }
+ mAttributesMap.put(key, props);
+ }
+ return props;
+ }
+
+ /**
+ * A {@link ChoiceProvder} which provides alternatives suitable for choosing
+ * values for a boolean property: true, false, or "default".
+ */
+ private static ChoiceProvider BOOLEAN_CHOICE_PROVIDER = new ChoiceProvider() {
+ @Override
+ public void addChoices(@NonNull List<String> titles, @NonNull List<URL> iconUrls,
+ @NonNull List<String> ids) {
+ titles.add("True");
+ ids.add(TRUE_ID);
+
+ titles.add("False");
+ ids.add(FALSE_ID);
+
+ titles.add(RuleAction.SEPARATOR);
+ ids.add(RuleAction.SEPARATOR);
+
+ titles.add("Default");
+ ids.add(CLEAR_ID);
+ }
+ };
+
+ /**
+ * A {@link ChoiceProvider} which provides the various available
+ * attribute values available for a given {@link Prop} property descriptor.
+ */
+ private static class EnumPropertyChoiceProvider implements ChoiceProvider {
+ private Prop mProperty;
+
+ public EnumPropertyChoiceProvider(Prop property) {
+ super();
+ mProperty = property;
+ }
+
+ @Override
+ public void addChoices(@NonNull List<String> titles, @NonNull List<URL> iconUrls,
+ @NonNull List<String> ids) {
+ for (Entry<String, String> entry : mProperty.getChoices().entrySet()) {
+ ids.add(entry.getKey());
+ titles.add(entry.getValue());
+ }
+
+ titles.add(RuleAction.SEPARATOR);
+ ids.add(RuleAction.SEPARATOR);
+
+ titles.add("Default");
+ ids.add(CLEAR_ID);
+ }
+ }
+
+ /**
+ * Returns true if the given node is "filled" (e.g. has layout width set to match
+ * parent or fill parent
+ */
+ protected final boolean isFilled(INode node, String attribute) {
+ String value = node.getStringAttr(ANDROID_URI, attribute);
+ return VALUE_MATCH_PARENT.equals(value) || VALUE_FILL_PARENT.equals(value);
+ }
+
+ /**
+ * Returns fill_parent or match_parent, depending on whether the minimum supported
+ * platform supports match_parent or not
+ *
+ * @return match_parent or fill_parent depending on which is supported by the project
+ */
+ protected final String getFillParentValueName() {
+ return supportsMatchParent() ? VALUE_MATCH_PARENT : VALUE_FILL_PARENT;
+ }
+
+ /**
+ * Returns true if the project supports match_parent instead of just fill_parent
+ *
+ * @return true if the project supports match_parent instead of just fill_parent
+ */
+ protected final boolean supportsMatchParent() {
+ // fill_parent was renamed match_parent in API level 8
+ return mRulesEngine.getMinApiLevel() >= 8;
+ }
+
+ /** Join strings into a single string with the given delimiter */
+ static String join(char delimiter, Collection<String> strings) {
+ StringBuilder sb = new StringBuilder(100);
+ for (String s : strings) {
+ if (sb.length() > 0) {
+ sb.append(delimiter);
+ }
+ sb.append(s);
+ }
+ return sb.toString();
+ }
+
+ static Map<String, String> concatenate(Map<String, String> pre, Map<String, String> post) {
+ Map<String, String> result = new HashMap<String, String>(pre.size() + post.size());
+ result.putAll(pre);
+ result.putAll(post);
+ return result;
+ }
+
+ // Quick utility for building up maps declaratively to minimize the diffs
+ static Map<String, String> mapify(String... values) {
+ Map<String, String> map = new HashMap<String, String>(values.length / 2);
+ for (int i = 0; i < values.length; i += 2) {
+ String key = values[i];
+ if (key == null) {
+ continue;
+ }
+ String value = values[i + 1];
+ map.put(key, value);
+ }
+
+ return map;
+ }
+
+ /**
+ * Produces a display name for an attribute, usually capitalizing the attribute name
+ * and splitting up underscores into new words
+ *
+ * @param name the attribute name to convert
+ * @return a display name for the attribute name
+ */
+ public static String getAttributeDisplayName(String name) {
+ if (name != null && name.length() > 0) {
+ StringBuilder sb = new StringBuilder();
+ boolean capitalizeNext = true;
+ for (int i = 0, n = name.length(); i < n; i++) {
+ char c = name.charAt(i);
+ if (capitalizeNext) {
+ c = Character.toUpperCase(c);
+ }
+ capitalizeNext = false;
+ if (c == '_') {
+ c = ' ';
+ capitalizeNext = true;
+ }
+ sb.append(c);
+ }
+
+ return sb.toString();
+ }
+
+ return name;
+ }
+
+
+ // ==== Paste support ====
+
+ /**
+ * Most views can't accept children so there's nothing to paste on them. In
+ * this case, defer the call to the parent layout and use the target node as
+ * an indication of where to paste.
+ */
+ @Override
+ public void onPaste(@NonNull INode targetNode, @Nullable Object targetView,
+ @NonNull IDragElement[] elements) {
+ //
+ INode parent = targetNode.getParent();
+ if (parent != null) {
+ String parentFqcn = parent.getFqcn();
+ IViewRule parentRule = mRulesEngine.loadRule(parentFqcn);
+
+ if (parentRule instanceof BaseLayoutRule) {
+ ((BaseLayoutRule) parentRule).onPasteBeforeChild(parent, targetView, targetNode,
+ elements);
+ }
+ }
+ }
+
+ /**
+ * Support class for the context menu code. Stores state about properties in
+ * the context menu.
+ */
+ private static class Prop {
+ private final boolean mToggle;
+ private final boolean mFlag;
+ private final String mTitle;
+ private final Map<String, String> mChoices;
+ private String mDefinedBy;
+
+ public Prop(String title, boolean isToggle, boolean isFlag, Map<String, String> choices,
+ String definedBy) {
+ mTitle = title;
+ mToggle = isToggle;
+ mFlag = isFlag;
+ mChoices = choices;
+ mDefinedBy = definedBy;
+ }
+
+ public String getDefinedBy() {
+ return mDefinedBy;
+ }
+
+ public Prop(String title, boolean isToggle, String definedBy) {
+ this(title, isToggle, false, null, definedBy);
+ }
+
+ private boolean isToggle() {
+ return mToggle;
+ }
+
+ private boolean isFlag() {
+ return mFlag && mChoices != null;
+ }
+
+ private boolean isEnum() {
+ return !mFlag && mChoices != null;
+ }
+
+ private String getTitle() {
+ return mTitle;
+ }
+
+ private Map<String, String> getChoices() {
+ return mChoices;
+ }
+
+ private boolean isStringEdit() {
+ return mChoices == null && !mToggle;
+ }
+ }
+
+ /**
+ * Returns a source attribute value which points to a sample image. This is typically
+ * used to provide an initial image shown on ImageButtons, etc. There is no guarantee
+ * that the source pointed to by this method actually exists.
+ *
+ * @return a source attribute to use for sample images, never null
+ */
+ protected final String getSampleImageSrc() {
+ // Builtin graphics available since v1:
+ return "@android:drawable/btn_star"; //$NON-NLS-1$
+ }
+
+ /**
+ * Strips the {@code @+id} or {@code @id} prefix off of the given id
+ *
+ * @param id attribute to be stripped
+ * @return the id name without the {@code @+id} or {@code @id} prefix
+ */
+ @NonNull
+ public static String stripIdPrefix(@Nullable String id) {
+ 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)) {
+ return id.substring(ID_PREFIX.length());
+ }
+ return id;
+ }
+
+ private static String ensureValidString(String value) {
+ if (value == null) {
+ value = ""; //$NON-NLS-1$
+ }
+ return value;
+ }
+ }
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/CalendarViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/CalendarViewRule.java
new file mode 100644
index 000000000..91684e2c5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/CalendarViewRule.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+
+/**
+ * An {@link IViewRule} for android.widget.CalendarView.
+ */
+public class CalendarViewRule extends BaseViewRule {
+
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ // CalendarViews need a lot of space, and the wrapping doesn't seem to work
+ // well anyway; it reports a much-to-small size than actually accommodates its
+ // content.
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, getFillParentValueName());
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, getFillParentValueName());
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/DatePickerRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/DatePickerRule.java
new file mode 100644
index 000000000..a635a9ad6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/DatePickerRule.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2010 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;
+
+public class DatePickerRule extends IgnoredLayoutRule {
+ // A DatePicker inherits from FrameLayout but is not a general purpose
+ // FrameLayout
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/DialerFilterRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/DialerFilterRule.java
new file mode 100644
index 000000000..606bbd86c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/DialerFilterRule.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_TEXT;
+import static com.android.SdkConstants.FQCN_EDIT_TEXT;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+
+/**
+ * An {@link IViewRule} for android.widget.DialerFilterRule.
+ */
+public class DialerFilterRule extends BaseViewRule {
+
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ // A DialerFilter requires a couple of nested EditTexts with fixed ids:
+ if (insertType.isCreate()) {
+ String fillParent = getFillParentValueName();
+ INode hint = node.appendChild(FQCN_EDIT_TEXT);
+ hint.setAttribute(ANDROID_URI, ATTR_TEXT, "Hint");
+ hint.setAttribute(ANDROID_URI, ATTR_ID, "@android:id/hint"); //$NON-NLS-1$
+ hint.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent);
+
+ INode primary = node.appendChild(FQCN_EDIT_TEXT);
+ primary.setAttribute(ANDROID_URI, ATTR_TEXT, "Primary");
+ primary.setAttribute(ANDROID_URI, ATTR_ID, "@android:id/primary"); //$NON-NLS-1$
+ primary.setAttribute(ANDROID_URI, ATTR_LAYOUT_BELOW,
+ "@android:id/hint"); //$NON-NLS-1$
+ primary.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent);
+
+
+ // What do we initialize the icon to?
+ //INode icon = node.appendChild("android.widget.ImageView"); //$NON-NLS-1$
+ //icon.setAttribute(ANDROID_URI, ATTR_ID, "@android:id/icon"); //$NON-NLS-1$
+ }
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/EditTextRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/EditTextRule.java
new file mode 100644
index 000000000..03a5bc04e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/EditTextRule.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_EMS;
+import static com.android.SdkConstants.REQUEST_FOCUS;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+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.RuleAction;
+
+import java.util.List;
+
+/**
+ * An {@link IViewRule} for android.widget.EditText.
+ */
+public class EditTextRule extends BaseViewRule {
+
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ if (parent != null) {
+ INode focus = findFocus(findRoot(parent));
+ if (focus == null) {
+ // Add <requestFocus>
+ node.appendChild(REQUEST_FOCUS);
+ }
+
+ if (parent.getBounds().w >= 320) {
+ node.setAttribute(ANDROID_URI, ATTR_EMS, "10"); //$NON-NLS-1$
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Adds a "Request Focus" menu item.
+ */
+ @Override
+ public void addContextMenuActions(@NonNull List<RuleAction> actions,
+ final @NonNull INode selectedNode) {
+ super.addContextMenuActions(actions, selectedNode);
+
+ final boolean hasFocus = hasFocus(selectedNode);
+ final String label = hasFocus ? "Clear Focus" : "Request Focus";
+
+ IMenuCallback onChange = new IMenuCallback() {
+ @Override
+ public void action(
+ @NonNull RuleAction menuAction,
+ @NonNull List<? extends INode> selectedNodes,
+ @Nullable String valueId,
+ @Nullable Boolean newValue) {
+ selectedNode.editXml(label, new INodeHandler() {
+ @Override
+ public void handle(@NonNull INode node) {
+ INode focus = findFocus(findRoot(node));
+ if (focus != null && focus.getParent() != null) {
+ focus.getParent().removeChild(focus);
+ }
+ if (!hasFocus) {
+ node.appendChild(REQUEST_FOCUS);
+ }
+ }
+ });
+ }
+ };
+
+ actions.add(RuleAction.createAction("_setfocus", label, onChange, //$NON-NLS-1$
+ null, 5, false /*supportsMultipleNodes*/));
+ actions.add(RuleAction.createSeparator(7));
+ }
+
+ /** Returns true if the given node currently has focus */
+ private static boolean hasFocus(INode node) {
+ INode focus = findFocus(node);
+ if (focus != null) {
+ return focus.getParent() == node;
+ }
+
+ return false;
+ }
+
+ /** Returns the root/top level node in the view hierarchy that contains the given node */
+ private static INode findRoot(INode node) {
+ // First find the parent
+ INode root = node;
+ while (root != null) {
+ INode parent = root.getParent();
+ if (parent == null) {
+ break;
+ } else {
+ root = parent;
+ }
+ }
+
+ return root;
+ }
+
+ /** Finds the focus node (not the node containing focus, but the actual request focus node
+ * under a given node */
+ private static INode findFocus(INode node) {
+ if (node.getFqcn().equals(REQUEST_FOCUS)) {
+ return node;
+ }
+
+ for (INode child : node.getChildren()) {
+ INode focus = findFocus(child);
+ if (focus != null) {
+ return focus;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FragmentRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FragmentRule.java
new file mode 100644
index 000000000..f99cf0ceb
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FragmentRule.java
@@ -0,0 +1,46 @@
+/*
+ * 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_NAME;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+
+/**
+ * An {@link IViewRule} for the special XML {@code <fragment>} tag.
+ */
+public class FragmentRule extends BaseViewRule {
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ // When dropping a fragment tag, ask the user which class to use.
+ if (insertType == InsertType.CREATE) { // NOT InsertType.CREATE_PREVIEW
+ String fqcn = mRulesEngine.displayFragmentSourceInput();
+ if (fqcn != null) {
+ node.editXml("Add Fragment",
+ new PropertySettingNodeHandler(ANDROID_URI, ATTR_NAME,
+ fqcn.length() > 0 ? fqcn : null));
+ } else {
+ // Remove the view; the insertion was canceled
+ parent.removeChild(node);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FrameLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FrameLayoutRule.java
new file mode 100644
index 000000000..0f9096294
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FrameLayoutRule.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.DropFeedback;
+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.INode;
+import com.android.ide.common.api.INodeHandler;
+import com.android.ide.common.api.IViewMetadata;
+import com.android.ide.common.api.IViewMetadata.FillPreference;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+import com.android.ide.common.api.Point;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.RuleAction;
+import com.android.utils.Pair;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An {@link IViewRule} for android.widget.FrameLayout and all its derived
+ * classes.
+ */
+public class FrameLayoutRule extends BaseLayoutRule {
+
+ // ==== Drag'n'drop support ====
+ // The FrameLayout accepts any drag'n'drop anywhere on its surface.
+
+ @Override
+ public DropFeedback onDropEnter(@NonNull INode targetNode, @Nullable Object targetView,
+ final @Nullable IDragElement[] elements) {
+ if (elements.length == 0) {
+ return null;
+ }
+
+ return new DropFeedback(null, new IFeedbackPainter() {
+ @Override
+ public void paint(@NonNull IGraphics gc, @NonNull INode node,
+ @NonNull DropFeedback feedback) {
+ drawFeedback(gc, node, elements, feedback);
+ }
+ });
+ }
+
+ protected void drawFeedback(
+ IGraphics gc,
+ INode targetNode,
+ IDragElement[] elements,
+ DropFeedback feedback) {
+ Rect b = targetNode.getBounds();
+ if (!b.isValid()) {
+ return;
+ }
+
+ gc.useStyle(DrawingStyle.DROP_RECIPIENT);
+ gc.drawRect(b);
+
+ // Get the drop point
+ Point p = (Point) feedback.userData;
+
+ if (p == null) {
+ return;
+ }
+
+ Rect be = elements[0].getBounds();
+
+ gc.useStyle(DrawingStyle.DROP_PREVIEW);
+ if (be.isValid()) {
+ // At least the first element has a bound. Draw rectangles
+ // for all dropped elements with valid bounds, offset at
+ // (0,0)
+ for (IDragElement it : elements) {
+ Rect currBounds = it.getBounds();
+ if (currBounds.isValid()) {
+ int offsetX = b.x - currBounds.x;
+ int offsetY = b.y - currBounds.y;
+ drawElement(gc, it, offsetX, offsetY);
+ }
+ }
+ } else {
+ // We don't have bounds for new elements. In this case
+ // just draw insert lines indicating the top left corner where
+ // the item will be placed
+
+ // +1: Place lines fully within the view (the stroke width is 2) to
+ // make
+ // it even more visually obvious
+ gc.drawLine(b.x + 1, b.y, b.x + 1, b.y + b.h);
+ gc.drawLine(b.x, b.y + 1, b.x + b.w, b.y + 1);
+ }
+ }
+
+ @Override
+ public DropFeedback onDropMove(@NonNull INode targetNode, @NonNull IDragElement[] elements,
+ @Nullable DropFeedback feedback, @NonNull Point p) {
+ feedback.userData = p;
+ feedback.requestPaint = true;
+ return feedback;
+ }
+
+ @Override
+ public void onDropLeave(@NonNull INode targetNode, @NonNull IDragElement[] elements,
+ @Nullable DropFeedback feedback) {
+ // ignore
+ }
+
+ @Override
+ public void onDropped(final @NonNull INode targetNode, final @NonNull IDragElement[] elements,
+ final @Nullable DropFeedback feedback, final @NonNull Point p) {
+ Rect b = targetNode.getBounds();
+ if (!b.isValid()) {
+ return;
+ }
+
+ // 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 FrameLayout", new INodeHandler() {
+
+ @Override
+ public void handle(@NonNull INode node) {
+
+ // Now write the new elements.
+ for (IDragElement element : elements) {
+ String fqcn = element.getFqcn();
+
+ INode newChild = targetNode.appendChild(fqcn);
+
+ // Copy all the attributes, modifying them as needed.
+ addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER);
+
+ addInnerElements(newChild, element, idMap);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void addLayoutActions(
+ @NonNull List<RuleAction> actions,
+ final @NonNull INode parentNode,
+ final @NonNull List<? extends INode> children) {
+ super.addLayoutActions(actions, parentNode, children);
+ actions.add(RuleAction.createSeparator(25));
+ actions.add(createMarginAction(parentNode, children));
+ if (children != null && children.size() > 0) {
+ actions.add(createGravityAction(children, ATTR_LAYOUT_GRAVITY));
+ }
+ }
+
+ @Override
+ public void onChildInserted(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ // Look at the fill preferences and fill embedded layouts etc
+ String fqcn = node.getFqcn();
+ IViewMetadata metadata = mRulesEngine.getMetadata(fqcn);
+ if (metadata != null) {
+ FillPreference fill = metadata.getFillPreference();
+ String fillParent = getFillParentValueName();
+ if (fill.fillHorizontally(true)) {
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent);
+ }
+ if (fill.fillVertically(false)) {
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, fillParent);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GravityHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GravityHelper.java
new file mode 100644
index 000000000..b9aabad3f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GravityHelper.java
@@ -0,0 +1,233 @@
+/*
+ * 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
+import static com.android.SdkConstants.GRAVITY_VALUE_BOTTOM;
+import static com.android.SdkConstants.GRAVITY_VALUE_CENTER;
+import static com.android.SdkConstants.GRAVITY_VALUE_CENTER_HORIZONTAL;
+import static com.android.SdkConstants.GRAVITY_VALUE_CENTER_VERTICAL;
+import static com.android.SdkConstants.GRAVITY_VALUE_FILL;
+import static com.android.SdkConstants.GRAVITY_VALUE_FILL_HORIZONTAL;
+import static com.android.SdkConstants.GRAVITY_VALUE_FILL_VERTICAL;
+import static com.android.SdkConstants.GRAVITY_VALUE_LEFT;
+import static com.android.SdkConstants.GRAVITY_VALUE_RIGHT;
+import static com.android.SdkConstants.GRAVITY_VALUE_TOP;
+
+import org.w3c.dom.Element;
+
+/** Helper class for looking up the gravity masks of gravity attributes */
+public class GravityHelper {
+ // From SDK constants; temporary
+ public static final String GRAVITY_VALUE_START = "start"; //$NON-NLS-1$
+ public static final String GRAVITY_VALUE_END = "end"; //$NON-NLS-1$
+
+ /** Bitmask for a gravity which includes left */
+ @SuppressWarnings("PointlessBitwiseExpression") // for symmetry with other fields
+ public static final int GRAVITY_LEFT = 1 << 0;
+
+ /** Bitmask for a gravity which includes right */
+ public static final int GRAVITY_RIGHT = 1 << 1;
+
+ /** Bitmask for a gravity which includes center horizontal */
+ public static final int GRAVITY_CENTER_HORIZ = 1 << 2;
+
+ /** Bitmask for a gravity which includes fill horizontal */
+ public static final int GRAVITY_FILL_HORIZ = 1 << 3;
+
+ /** Bitmask for a gravity which includes center vertical */
+ public static final int GRAVITY_CENTER_VERT = 1 << 4;
+
+ /** Bitmask for a gravity which includes fill vertical */
+ public static final int GRAVITY_FILL_VERT = 1 << 5;
+
+ /** Bitmask for a gravity which includes top */
+ public static final int GRAVITY_TOP = 1 << 6;
+
+ /** Bitmask for a gravity which includes bottom */
+ public static final int GRAVITY_BOTTOM = 1 << 7;
+
+ /** Bitmask for a gravity which includes start */
+ public static final int GRAVITY_START = 1 << 8;
+
+ /** Bitmask for a gravity which includes end */
+ public static final int GRAVITY_END = 1 << 9;
+
+ /** Bitmask for a gravity which includes any horizontal constraint */
+ public static final int GRAVITY_HORIZ_MASK = GRAVITY_CENTER_HORIZ | GRAVITY_FILL_HORIZ
+ | GRAVITY_LEFT | GRAVITY_RIGHT | GRAVITY_START | GRAVITY_END;
+
+ /** Bitmask for a gravity which any vertical constraint */
+ public static final int GRAVITY_VERT_MASK = GRAVITY_CENTER_VERT | GRAVITY_FILL_VERT
+ | GRAVITY_TOP | GRAVITY_BOTTOM;
+
+ /**
+ * Returns the gravity of the given element
+ *
+ * @param element the element to look up the gravity for
+ * @return a bit mask corresponding to the selected gravities
+ */
+ public static int getGravity(Element element) {
+ String gravityString = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY);
+ return getGravity(gravityString, GRAVITY_LEFT | GRAVITY_TOP);
+ }
+
+ /**
+ * Returns the gravity bitmask for the given gravity string description
+ *
+ * @param gravityString the gravity string description
+ * @param defaultMask the default/initial bitmask to start with
+ * @return a bitmask corresponding to the gravity description
+ */
+ public static int getGravity(String gravityString, int defaultMask) {
+ int gravity = defaultMask;
+ if (gravityString != null && !gravityString.isEmpty()) {
+ String[] anchors = gravityString.split("\\|"); //$NON-NLS-1$
+ for (String anchor : anchors) {
+ if (GRAVITY_VALUE_CENTER.equals(anchor)) {
+ gravity = GRAVITY_CENTER_HORIZ | GRAVITY_CENTER_VERT;
+ } else if (GRAVITY_VALUE_FILL.equals(anchor)) {
+ gravity = GRAVITY_FILL_HORIZ | GRAVITY_FILL_VERT;
+ } else if (GRAVITY_VALUE_CENTER_VERTICAL.equals(anchor)) {
+ gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_CENTER_VERT;
+ } else if (GRAVITY_VALUE_CENTER_HORIZONTAL.equals(anchor)) {
+ gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_CENTER_HORIZ;
+ } else if (GRAVITY_VALUE_FILL_VERTICAL.equals(anchor)) {
+ gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_FILL_VERT;
+ } else if (GRAVITY_VALUE_FILL_HORIZONTAL.equals(anchor)) {
+ gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_FILL_HORIZ;
+ } else if (GRAVITY_VALUE_TOP.equals(anchor)) {
+ gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_TOP;
+ } else if (GRAVITY_VALUE_BOTTOM.equals(anchor)) {
+ gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_BOTTOM;
+ } else if (GRAVITY_VALUE_LEFT.equals(anchor)) {
+ gravity = (gravity & (GRAVITY_VERT_MASK|GRAVITY_START)) | GRAVITY_LEFT;
+ } else if (GRAVITY_VALUE_RIGHT.equals(anchor)) {
+ gravity = (gravity & (GRAVITY_VERT_MASK|GRAVITY_END)) | GRAVITY_RIGHT;
+ } else if (GRAVITY_VALUE_START.equals(anchor)) {
+ gravity = (gravity & (GRAVITY_VERT_MASK|GRAVITY_LEFT)) | GRAVITY_START;
+ } else if (GRAVITY_VALUE_END.equals(anchor)) {
+ gravity = (gravity & (GRAVITY_VERT_MASK|GRAVITY_RIGHT)) | GRAVITY_END;
+ } // else: "clip" not supported
+ }
+ }
+
+ return gravity;
+ }
+
+ /**
+ * Returns true if the given gravity bitmask is constrained horizontally
+ *
+ * @param gravity the gravity bitmask
+ * @return true if the given gravity bitmask is constrained horizontally
+ */
+ public static boolean isConstrainedHorizontally(int gravity) {
+ return (gravity & GRAVITY_HORIZ_MASK) != 0;
+ }
+
+ /**
+ * Returns true if the given gravity bitmask is constrained vertically
+ *
+ * @param gravity the gravity bitmask
+ * @return true if the given gravity bitmask is constrained vertically
+ */
+ public static boolean isConstrainedVertically(int gravity) {
+ return (gravity & GRAVITY_VERT_MASK) != 0;
+ }
+
+ /**
+ * Returns true if the given gravity bitmask is left aligned
+ *
+ * @param gravity the gravity bitmask
+ * @return true if the given gravity bitmask is left aligned
+ */
+ public static boolean isLeftAligned(int gravity) {
+ return (gravity & (GRAVITY_LEFT|GRAVITY_START)) != 0;
+ }
+
+ /**
+ * Returns true if the given gravity bitmask is top aligned
+ *
+ * @param gravity the gravity bitmask
+ * @return true if the given gravity bitmask is aligned
+ */
+ public static boolean isTopAligned(int gravity) {
+ return (gravity & GRAVITY_TOP) != 0;
+ }
+
+ /** Returns a gravity value string from the given gravity bitmask
+ *
+ * @param gravity the gravity bitmask
+ * @return the corresponding gravity string suitable as an XML attribute value
+ */
+ public static String getGravity(int gravity) {
+ if (gravity == 0) {
+ return "";
+ }
+
+ if ((gravity & (GRAVITY_CENTER_HORIZ | GRAVITY_CENTER_VERT)) ==
+ (GRAVITY_CENTER_HORIZ | GRAVITY_CENTER_VERT)) {
+ return GRAVITY_VALUE_CENTER;
+ }
+
+ StringBuilder sb = new StringBuilder(30);
+ int horizontal = gravity & GRAVITY_HORIZ_MASK;
+ int vertical = gravity & GRAVITY_VERT_MASK;
+
+ if ((horizontal & (GRAVITY_LEFT|GRAVITY_START)) != 0) {
+ if ((horizontal & GRAVITY_LEFT) != 0) {
+ sb.append(GRAVITY_VALUE_LEFT);
+ }
+ if ((horizontal & GRAVITY_START) != 0) {
+ if (sb.length() > 0) {
+ sb.append('|');
+ }
+ sb.append(GRAVITY_VALUE_START);
+ }
+ } else if ((horizontal & (GRAVITY_RIGHT|GRAVITY_END)) != 0) {
+ if ((horizontal & GRAVITY_RIGHT) != 0) {
+ sb.append(GRAVITY_VALUE_RIGHT);
+ }
+ if ((horizontal & GRAVITY_END) != 0) {
+ if (sb.length() > 0) {
+ sb.append('|');
+ }
+ sb.append(GRAVITY_VALUE_END);
+ }
+ } else if ((horizontal & GRAVITY_CENTER_HORIZ) != 0) {
+ sb.append(GRAVITY_VALUE_CENTER_HORIZONTAL);
+ } else if ((horizontal & GRAVITY_FILL_HORIZ) != 0) {
+ sb.append(GRAVITY_VALUE_FILL_HORIZONTAL);
+ }
+
+ if (sb.length() > 0 && vertical != 0) {
+ sb.append('|');
+ }
+
+ if ((vertical & GRAVITY_TOP) != 0) {
+ sb.append(GRAVITY_VALUE_TOP);
+ } else if ((vertical & GRAVITY_BOTTOM) != 0) {
+ sb.append(GRAVITY_VALUE_BOTTOM);
+ } else if ((vertical & GRAVITY_CENTER_VERT) != 0) {
+ sb.append(GRAVITY_VALUE_CENTER_VERTICAL);
+ } else if ((vertical & GRAVITY_FILL_VERT) != 0) {
+ sb.append(GRAVITY_VALUE_FILL_VERTICAL);
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java
new file mode 100644
index 000000000..80a23c6db
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java
@@ -0,0 +1,676 @@
+/*
+ * 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
+import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
+import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
+import static com.android.SdkConstants.ATTR_ORIENTATION;
+import static com.android.SdkConstants.FQCN_GRID_LAYOUT;
+import static com.android.SdkConstants.FQCN_SPACE;
+import static com.android.SdkConstants.FQCN_SPACE_V7;
+import static com.android.SdkConstants.GRAVITY_VALUE_FILL;
+import static com.android.SdkConstants.GRAVITY_VALUE_FILL_HORIZONTAL;
+import static com.android.SdkConstants.GRAVITY_VALUE_FILL_VERTICAL;
+import static com.android.SdkConstants.GRAVITY_VALUE_LEFT;
+import static com.android.SdkConstants.GRID_LAYOUT;
+import static com.android.SdkConstants.VALUE_HORIZONTAL;
+import static com.android.SdkConstants.VALUE_TRUE;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.DropFeedback;
+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.IViewMetadata;
+import com.android.ide.common.api.IViewMetadata.FillPreference;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+import com.android.ide.common.api.Point;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.api.RuleAction.Choices;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.common.layout.grid.GridDropHandler;
+import com.android.ide.common.layout.grid.GridLayoutPainter;
+import com.android.ide.common.layout.grid.GridModel;
+import com.android.ide.common.layout.grid.GridModel.ViewData;
+import com.android.utils.Pair;
+
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An {@link IViewRule} for android.widget.GridLayout which provides designtime
+ * interaction with GridLayouts.
+ * <p>
+ * TODO:
+ * <ul>
+ * <li>Handle multi-drag: preserving relative positions and alignments among dragged
+ * views.
+ * <li>Handle GridLayouts that have been configured in a vertical orientation.
+ * <li>Handle free-form editing GridLayouts that have been manually edited rather than
+ * built up using free-form editing (e.g. they might not follow the same spacing
+ * convention, might use weights etc)
+ * <li>Avoid setting row and column numbers on the actual elements if they can be skipped
+ * to make the XML leaner.
+ * </ul>
+ */
+public class GridLayoutRule extends BaseLayoutRule {
+ /**
+ * The size of the visual regular grid that we snap to (if {@link #sSnapToGrid} is set
+ */
+ public static final int GRID_SIZE = 16;
+
+ /** Standard gap between views */
+ public static final int SHORT_GAP_DP = 16;
+
+ /**
+ * The preferred margin size, in pixels
+ */
+ public static final int MARGIN_SIZE = 32;
+
+ /**
+ * Size in screen pixels in the IDE of the gutter shown for new rows and columns (in
+ * grid mode)
+ */
+ private static final int NEW_CELL_WIDTH = 10;
+
+ /**
+ * Maximum size of a widget relative to a cell which is allowed to fit into a cell
+ * (and thereby enlarge it) before it is spread with row or column spans.
+ */
+ public static final double MAX_CELL_DIFFERENCE = 1.2;
+
+ /** Whether debugging diagnostics is available in the toolbar */
+ private static final boolean CAN_DEBUG =
+ VALUE_TRUE.equals(System.getenv("ADT_DEBUG_GRIDLAYOUT")); //$NON-NLS-1$
+
+ private static final String ACTION_ADD_ROW = "_addrow"; //$NON-NLS-1$
+ private static final String ACTION_REMOVE_ROW = "_removerow"; //$NON-NLS-1$
+ private static final String ACTION_ADD_COL = "_addcol"; //$NON-NLS-1$
+ private static final String ACTION_REMOVE_COL = "_removecol"; //$NON-NLS-1$
+ private static final String ACTION_ORIENTATION = "_orientation"; //$NON-NLS-1$
+ private static final String ACTION_SHOW_STRUCTURE = "_structure"; //$NON-NLS-1$
+ private static final String ACTION_GRID_MODE = "_gridmode"; //$NON-NLS-1$
+ private static final String ACTION_SNAP = "_snap"; //$NON-NLS-1$
+ private static final String ACTION_DEBUG = "_debug"; //$NON-NLS-1$
+
+ private static final URL ICON_HORIZONTAL = GridLayoutRule.class.getResource("hlinear.png"); //$NON-NLS-1$
+ private static final URL ICON_VERTICAL = GridLayoutRule.class.getResource("vlinear.png"); //$NON-NLS-1$
+ private static final URL ICON_ADD_ROW = GridLayoutRule.class.getResource("addrow.png"); //$NON-NLS-1$
+ private static final URL ICON_REMOVE_ROW = GridLayoutRule.class.getResource("removerow.png"); //$NON-NLS-1$
+ private static final URL ICON_ADD_COL = GridLayoutRule.class.getResource("addcol.png"); //$NON-NLS-1$
+ private static final URL ICON_REMOVE_COL = GridLayoutRule.class.getResource("removecol.png"); //$NON-NLS-1$
+ private static final URL ICON_SHOW_STRUCT = GridLayoutRule.class.getResource("showgrid.png"); //$NON-NLS-1$
+ private static final URL ICON_GRID_MODE = GridLayoutRule.class.getResource("gridmode.png"); //$NON-NLS-1$
+ private static final URL ICON_SNAP = GridLayoutRule.class.getResource("snap.png"); //$NON-NLS-1$
+
+ /**
+ * Whether the IDE should show diagnostics for debugging the grid layout - including
+ * spacers visibly in the outline, showing row and column numbers, and so on
+ */
+ public static boolean sDebugGridLayout = CAN_DEBUG;
+
+ /** Whether the structure (grid model) should be displayed persistently to the user */
+ public static boolean sShowStructure = false;
+
+ /** Whether the drop positions should snap to a regular grid */
+ public static boolean sSnapToGrid = false;
+
+ /**
+ * Whether the grid is edited in "grid mode" where the operations are row/column based
+ * rather than free-form
+ */
+ public static boolean sGridMode = true;
+
+ /** Constructs a new {@link GridLayoutRule} */
+ public GridLayoutRule() {
+ }
+
+ @Override
+ public void addLayoutActions(
+ @NonNull List<RuleAction> actions,
+ final @NonNull INode parentNode,
+ final @NonNull List<? extends INode> children) {
+ super.addLayoutActions(actions, parentNode, children);
+
+ String namespace = getNamespace(parentNode);
+ Choices orientationAction = RuleAction.createChoices(
+ ACTION_ORIENTATION,
+ "Orientation", //$NON-NLS-1$
+ new PropertyCallback(Collections.singletonList(parentNode),
+ "Change LinearLayout Orientation", namespace, ATTR_ORIENTATION), Arrays
+ .<String> asList("Set Horizontal Orientation", "Set Vertical Orientation"),
+ Arrays.<URL> asList(ICON_HORIZONTAL, ICON_VERTICAL), Arrays.<String> asList(
+ "horizontal", "vertical"), getCurrentOrientation(parentNode),
+ null /* icon */, -10, false);
+ orientationAction.setRadio(true);
+ actions.add(orientationAction);
+
+ // Gravity and margins
+ if (children != null && children.size() > 0) {
+ actions.add(RuleAction.createSeparator(35));
+ actions.add(createMarginAction(parentNode, children));
+ actions.add(createGravityAction(children, ATTR_LAYOUT_GRAVITY));
+ }
+
+ IMenuCallback actionCallback = new IMenuCallback() {
+ @Override
+ public void action(
+ final @NonNull RuleAction action,
+ @NonNull List<? extends INode> selectedNodes,
+ final @Nullable String valueId,
+ final @Nullable Boolean newValue) {
+ parentNode.editXml("Add/Remove Row/Column", new INodeHandler() {
+ @Override
+ public void handle(@NonNull INode n) {
+ String id = action.getId();
+ if (id.equals(ACTION_SHOW_STRUCTURE)) {
+ sShowStructure = !sShowStructure;
+ mRulesEngine.redraw();
+ return;
+ } else if (id.equals(ACTION_GRID_MODE)) {
+ sGridMode = !sGridMode;
+ mRulesEngine.redraw();
+ return;
+ } else if (id.equals(ACTION_SNAP)) {
+ sSnapToGrid = !sSnapToGrid;
+ mRulesEngine.redraw();
+ return;
+ } else if (id.equals(ACTION_DEBUG)) {
+ sDebugGridLayout = !sDebugGridLayout;
+ mRulesEngine.layout();
+ return;
+ }
+
+ GridModel grid = GridModel.get(mRulesEngine, parentNode, null);
+ if (id.equals(ACTION_ADD_ROW)) {
+ grid.addRow(children);
+ } else if (id.equals(ACTION_REMOVE_ROW)) {
+ grid.removeRows(children);
+ } else if (id.equals(ACTION_ADD_COL)) {
+ grid.addColumn(children);
+ } else if (id.equals(ACTION_REMOVE_COL)) {
+ grid.removeColumns(children);
+ }
+ }
+
+ });
+ }
+ };
+
+ actions.add(RuleAction.createSeparator(142));
+
+ actions.add(RuleAction.createToggle(ACTION_GRID_MODE, "Grid Model Mode",
+ sGridMode, actionCallback, ICON_GRID_MODE, 145, false));
+
+ // Add and Remove Column actions only apply in Grid Mode
+ if (sGridMode) {
+ actions.add(RuleAction.createToggle(ACTION_SHOW_STRUCTURE, "Show Structure",
+ sShowStructure, actionCallback, ICON_SHOW_STRUCT, 147, false));
+
+ // Add Row and Add Column
+ actions.add(RuleAction.createSeparator(150));
+ actions.add(RuleAction.createAction(ACTION_ADD_COL, "Add Column", actionCallback,
+ ICON_ADD_COL, 160, false /* supportsMultipleNodes */));
+ actions.add(RuleAction.createAction(ACTION_ADD_ROW, "Add Row", actionCallback,
+ ICON_ADD_ROW, 165, false));
+
+ // Remove Row and Remove Column (if something is selected)
+ if (children != null && children.size() > 0) {
+ // TODO: Add "Merge Columns" and "Merge Rows" ?
+
+ actions.add(RuleAction.createAction(ACTION_REMOVE_COL, "Remove Column",
+ actionCallback, ICON_REMOVE_COL, 170, false));
+ actions.add(RuleAction.createAction(ACTION_REMOVE_ROW, "Remove Row",
+ actionCallback, ICON_REMOVE_ROW, 175, false));
+ }
+
+ actions.add(RuleAction.createSeparator(185));
+ } else {
+ actions.add(RuleAction.createToggle(ACTION_SHOW_STRUCTURE, "Show Structure",
+ sShowStructure, actionCallback, ICON_SHOW_STRUCT, 190, false));
+
+ // Snap to Grid and Show Structure are only relevant in free form mode
+ actions.add(RuleAction.createToggle(ACTION_SNAP, "Snap to Grid",
+ sSnapToGrid, actionCallback, ICON_SNAP, 200, false));
+ }
+
+ // Temporary: Diagnostics for GridLayout
+ if (CAN_DEBUG) {
+ actions.add(RuleAction.createToggle(ACTION_DEBUG, "Debug",
+ sDebugGridLayout, actionCallback, null, 210, false));
+ }
+ }
+
+ /**
+ * Returns the orientation attribute value currently used by the node (even if not
+ * defined, in which case the default horizontal value is returned)
+ */
+ private String getCurrentOrientation(final INode node) {
+ String orientation = node.getStringAttr(getNamespace(node), ATTR_ORIENTATION);
+ if (orientation == null || orientation.length() == 0) {
+ orientation = VALUE_HORIZONTAL;
+ }
+ return orientation;
+ }
+
+ @Override
+ public DropFeedback onDropEnter(@NonNull INode targetNode, @Nullable Object targetView,
+ @Nullable IDragElement[] elements) {
+ GridDropHandler userData = new GridDropHandler(this, targetNode, targetView);
+ IFeedbackPainter painter = GridLayoutPainter.createDropFeedbackPainter(this, elements);
+ return new DropFeedback(userData, painter);
+ }
+
+ @Override
+ public DropFeedback onDropMove(@NonNull INode targetNode, @NonNull IDragElement[] elements,
+ @Nullable DropFeedback feedback, @NonNull Point p) {
+ if (feedback == null) {
+ return null;
+ }
+ feedback.requestPaint = true;
+
+ GridDropHandler handler = (GridDropHandler) feedback.userData;
+ handler.computeMatches(feedback, p);
+
+ return feedback;
+ }
+
+ @Override
+ public void onDropped(final @NonNull INode targetNode, final @NonNull IDragElement[] elements,
+ @Nullable DropFeedback feedback, @NonNull Point p) {
+ if (feedback == null) {
+ return;
+ }
+
+ Rect b = targetNode.getBounds();
+ if (!b.isValid()) {
+ return;
+ }
+
+ GridDropHandler dropHandler = (GridDropHandler) feedback.userData;
+ if (dropHandler.getRowMatch() == null || dropHandler.getColumnMatch() == null) {
+ return;
+ }
+
+ // Collect IDs from dropped elements and remap them to new IDs
+ // if this is a copy or from a different canvas.
+ Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements,
+ feedback.isCopy || !feedback.sameCanvas);
+
+ for (IDragElement element : elements) {
+ INode newChild;
+ if (!sGridMode) {
+ newChild = dropHandler.handleFreeFormDrop(targetNode, element);
+ } else {
+ newChild = dropHandler.handleGridModeDrop(targetNode, element);
+ }
+
+ // Copy all the attributes, modifying them as needed.
+ addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER);
+
+ addInnerElements(newChild, element, idMap);
+ }
+ }
+
+ @Override
+ public void onChildInserted(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ if (insertType == InsertType.MOVE_WITHIN) {
+ // Don't adjust widths/heights/weights when just moving within a single layout
+ return;
+ }
+
+ if (GridModel.isSpace(node.getFqcn())) {
+ return;
+ }
+
+ // Attempt to set "fill" properties on newly added views such that for example
+ // a text field will stretch horizontally.
+ String fqcn = node.getFqcn();
+ IViewMetadata metadata = mRulesEngine.getMetadata(fqcn);
+ FillPreference fill = metadata.getFillPreference();
+ String gravity = computeDefaultGravity(fill);
+ if (gravity != null) {
+ node.setAttribute(getNamespace(parent), ATTR_LAYOUT_GRAVITY, gravity);
+ }
+ }
+
+ /**
+ * Returns the namespace URI to use for GridLayout-specific attributes, such
+ * as columnCount, layout_column, layout_column_span, layout_gravity etc.
+ *
+ * @param layout the GridLayout instance to look up the namespace for
+ * @return the namespace, never null
+ */
+ public String getNamespace(INode layout) {
+ String namespace = ANDROID_URI;
+
+ String fqcn = layout.getFqcn();
+ if (!fqcn.equals(GRID_LAYOUT) && !fqcn.equals(FQCN_GRID_LAYOUT)) {
+ namespace = mRulesEngine.getAppNameSpace();
+ }
+
+ return namespace;
+ }
+
+ /**
+ * Computes the default gravity to be used for a widget of the given fill
+ * preference when added to a grid layout
+ *
+ * @param fill the fill preference for the widget
+ * @return the gravity value, or null, to be set on the widget
+ */
+ public static String computeDefaultGravity(FillPreference fill) {
+ String horizontal = GRAVITY_VALUE_LEFT;
+ String vertical = null;
+ if (fill.fillHorizontally(true /*verticalContext*/)) {
+ horizontal = GRAVITY_VALUE_FILL_HORIZONTAL;
+ }
+ if (fill.fillVertically(true /*verticalContext*/)) {
+ vertical = GRAVITY_VALUE_FILL_VERTICAL;
+ }
+ String gravity;
+ if (horizontal == GRAVITY_VALUE_FILL_HORIZONTAL
+ && vertical == GRAVITY_VALUE_FILL_VERTICAL) {
+ gravity = GRAVITY_VALUE_FILL;
+ } else if (vertical != null) {
+ gravity = horizontal + '|' + vertical;
+ } else {
+ gravity = horizontal;
+ }
+
+ return gravity;
+ }
+
+ @Override
+ public void onRemovingChildren(@NonNull List<INode> deleted, @NonNull INode parent,
+ boolean moved) {
+ super.onRemovingChildren(deleted, parent, moved);
+
+ if (!sGridMode) {
+ // Attempt to clean up spacer objects for any newly-empty rows or columns
+ // as the result of this deletion
+ GridModel grid = GridModel.get(mRulesEngine, parent, null);
+ grid.onDeleted(deleted);
+ }
+ }
+
+ @Override
+ protected void paintResizeFeedback(IGraphics gc, INode node, ResizeState state) {
+ if (!sGridMode) {
+ GridModel grid = getGrid(state);
+ GridLayoutPainter.paintResizeFeedback(gc, state.layout, grid);
+ }
+
+ if (resizingWidget(state)) {
+ super.paintResizeFeedback(gc, node, state);
+ } else {
+ GridModel grid = getGrid(state);
+ int startColumn = grid.getColumn(state.bounds.x);
+ int endColumn = grid.getColumn(state.bounds.x2());
+ int columnSpan = endColumn - startColumn + 1;
+
+ int startRow = grid.getRow(state.bounds.y);
+ int endRow = grid.getRow(state.bounds.y2());
+ int rowSpan = endRow - startRow + 1;
+
+ Rect cellBounds = grid.getCellBounds(startRow, startColumn, rowSpan, columnSpan);
+ gc.useStyle(DrawingStyle.RESIZE_PREVIEW);
+ gc.drawRect(cellBounds);
+ }
+ }
+
+ /** Returns the grid size cached on the given {@link ResizeState} object */
+ private GridModel getGrid(ResizeState resizeState) {
+ GridModel grid = (GridModel) resizeState.clientData;
+ if (grid == null) {
+ grid = GridModel.get(mRulesEngine, resizeState.layout, resizeState.layoutView);
+ resizeState.clientData = grid;
+ }
+
+ return grid;
+ }
+
+ @Override
+ protected void setNewSizeBounds(ResizeState state, INode node, INode layout,
+ Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) {
+
+ if (resizingWidget(state)) {
+ if (state.fillWidth || state.fillHeight || state.wrapWidth || state.wrapHeight) {
+ GridModel grid = getGrid(state);
+ ViewData view = grid.getView(node);
+ if (view != null) {
+ String gravityString = grid.getGridAttribute(view.node, ATTR_LAYOUT_GRAVITY);
+ int gravity = GravityHelper.getGravity(gravityString, 0);
+ if (view.column > 0 && verticalEdge != null && state.fillWidth) {
+ state.fillWidth = false;
+ state.wrapWidth = true;
+ gravity &= ~GravityHelper.GRAVITY_HORIZ_MASK;
+ gravity |= GravityHelper.GRAVITY_FILL_HORIZ;
+ } else if (verticalEdge != null && state.wrapWidth) {
+ gravity &= ~GravityHelper.GRAVITY_HORIZ_MASK;
+ gravity |= GravityHelper.GRAVITY_LEFT;
+ }
+ if (view.row > 0 && horizontalEdge != null && state.fillHeight) {
+ state.fillHeight = false;
+ state.wrapHeight = true;
+ gravity &= ~GravityHelper.GRAVITY_VERT_MASK;
+ gravity |= GravityHelper.GRAVITY_FILL_VERT;
+ } else if (horizontalEdge != null && state.wrapHeight) {
+ gravity &= ~GravityHelper.GRAVITY_VERT_MASK;
+ gravity |= GravityHelper.GRAVITY_TOP;
+ }
+ gravityString = GravityHelper.getGravity(gravity);
+ grid.setGridAttribute(view.node, ATTR_LAYOUT_GRAVITY, gravityString);
+ // Fall through and set layout_width and/or layout_height to wrap_content
+ }
+ }
+ super.setNewSizeBounds(state, node, layout, oldBounds, newBounds, horizontalEdge,
+ verticalEdge);
+ } else {
+ Pair<Integer, Integer> spans = computeResizeSpans(state);
+ int rowSpan = spans.getFirst();
+ int columnSpan = spans.getSecond();
+ GridModel grid = getGrid(state);
+ grid.setColumnSpanAttribute(node, columnSpan);
+ grid.setRowSpanAttribute(node, rowSpan);
+
+ ViewData view = grid.getView(node);
+ if (view != null) {
+ String gravityString = grid.getGridAttribute(view.node, ATTR_LAYOUT_GRAVITY);
+ int gravity = GravityHelper.getGravity(gravityString, 0);
+ if (verticalEdge != null && columnSpan > 1) {
+ gravity &= ~GravityHelper.GRAVITY_HORIZ_MASK;
+ gravity |= GravityHelper.GRAVITY_FILL_HORIZ;
+ }
+ if (horizontalEdge != null && rowSpan > 1) {
+ gravity &= ~GravityHelper.GRAVITY_VERT_MASK;
+ gravity |= GravityHelper.GRAVITY_FILL_VERT;
+ }
+ gravityString = GravityHelper.getGravity(gravity);
+ grid.setGridAttribute(view.node, ATTR_LAYOUT_GRAVITY, gravityString);
+ }
+ }
+ }
+
+ @Override
+ protected String getResizeUpdateMessage(ResizeState state, INode child, INode parent,
+ Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) {
+ Pair<Integer, Integer> spans = computeResizeSpans(state);
+ if (resizingWidget(state)) {
+ String width = state.getWidthAttribute();
+ String height = state.getHeightAttribute();
+
+ String message;
+ if (horizontalEdge == null) {
+ message = width;
+ } else if (verticalEdge == null) {
+ message = height;
+ } else {
+ // U+00D7: Unicode for multiplication sign
+ message = String.format("%s \u00D7 %s", width, height);
+ }
+
+ // Tack on a tip about using the Shift modifier key
+ return String.format("%s\n(Press Shift to resize row/column spans)", message);
+ } else {
+ int rowSpan = spans.getFirst();
+ int columnSpan = spans.getSecond();
+ return String.format("ColumnSpan=%d, RowSpan=%d\n(Release Shift to resize widget itself)",
+ columnSpan, rowSpan);
+ }
+ }
+
+ /**
+ * Returns true if we're resizing the widget, and false if we're resizing the cell
+ * spans
+ */
+ private static boolean resizingWidget(ResizeState state) {
+ return (state.modifierMask & DropFeedback.MODIFIER2) == 0;
+ }
+
+ /**
+ * Computes the new column and row spans as the result of the current resizing
+ * operation
+ */
+ private Pair<Integer, Integer> computeResizeSpans(ResizeState state) {
+ GridModel grid = getGrid(state);
+
+ int startColumn = grid.getColumn(state.bounds.x);
+ int endColumn = grid.getColumn(state.bounds.x2());
+ int columnSpan = endColumn - startColumn + 1;
+
+ int startRow = grid.getRow(state.bounds.y);
+ int endRow = grid.getRow(state.bounds.y2());
+ int rowSpan = endRow - startRow + 1;
+
+ return Pair.of(rowSpan, columnSpan);
+ }
+
+ /**
+ * Returns the size of the new cell gutter in layout coordinates
+ *
+ * @return the size of the new cell gutter in layout coordinates
+ */
+ public int getNewCellSize() {
+ return mRulesEngine.screenToLayout(NEW_CELL_WIDTH / 2);
+ }
+
+ @Override
+ public void paintSelectionFeedback(@NonNull IGraphics graphics, @NonNull INode parentNode,
+ @NonNull List<? extends INode> childNodes, @Nullable Object view) {
+ super.paintSelectionFeedback(graphics, parentNode, childNodes, view);
+
+ if (sShowStructure) {
+ // TODO: Cache the grid
+ if (view != null) {
+ if (GridLayoutPainter.paintStructure(view, DrawingStyle.GUIDELINE_DASHED,
+ parentNode, graphics)) {
+ return;
+ }
+ }
+ GridLayoutPainter.paintStructure(DrawingStyle.GUIDELINE_DASHED,
+ parentNode, graphics, GridModel.get(mRulesEngine, parentNode, view));
+ } else if (sDebugGridLayout) {
+ GridLayoutPainter.paintStructure(DrawingStyle.GRID,
+ parentNode, graphics, GridModel.get(mRulesEngine, parentNode, view));
+ }
+
+ // TBD: Highlight the cells around the selection, and display easy controls
+ // for for example tweaking the rowspan/colspan of a cell? (but only in grid mode)
+ }
+
+ /**
+ * Paste into a GridLayout. We have several possible behaviors (and many
+ * more than are listed here):
+ * <ol>
+ * <li> Preserve the current positions of the elements (if pasted from another
+ * canvas, not just XML markup copied from say a web site) and apply those
+ * into the current grid. This might mean "overwriting" (sitting on top of)
+ * existing elements.
+ * <li> Fill available "holes" in the grid.
+ * <li> Lay them out consecutively, row by row, like text.
+ * <li> Some hybrid approach, where I attempt to preserve the <b>relative</b>
+ * relationships (columns/wrapping, spacing between the pasted views etc)
+ * but I append them to the bottom of the layout on one or more new rows.
+ * <li> Try to paste at the current mouse position, if known, preserving the
+ * relative distances between the existing elements there.
+ * </ol>
+ * Attempting to preserve the current position isn't possible right now,
+ * because the clipboard data contains only the textual representation of
+ * the markup. (We'd need to stash position information from a previous
+ * layout render along with the clipboard data).
+ * <p>
+ * Currently, this implementation simply lays out the elements row by row,
+ * approach #3 above.
+ */
+ @Override
+ public void onPaste(
+ @NonNull INode targetNode,
+ @Nullable Object targetView,
+ @NonNull IDragElement[] elements) {
+ DropFeedback feedback = onDropEnter(targetNode, targetView, elements);
+ if (feedback != null) {
+ Rect b = targetNode.getBounds();
+ if (!b.isValid()) {
+ return;
+ }
+
+ Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements,
+ true /* remap id's */);
+
+ for (IDragElement element : elements) {
+ // Skip <Space> elements and only insert the real elements being
+ // copied
+ if (elements.length > 1 && (FQCN_SPACE.equals(element.getFqcn())
+ || FQCN_SPACE_V7.equals(element.getFqcn()))) {
+ continue;
+ }
+
+ String fqcn = element.getFqcn();
+ INode newChild = targetNode.appendChild(fqcn);
+ addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER);
+
+ // Ensure that we reset any potential row/column attributes from a different
+ // grid layout being copied from
+ GridDropHandler handler = (GridDropHandler) feedback.userData;
+ GridModel grid = handler.getGrid();
+ grid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, null);
+ grid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, null);
+
+ // TODO: Set columnSpans to avoid making these widgets completely
+ // break the layout
+ // Alternatively, I could just lay them all out on subsequent lines
+ // with a column span of columnSpan5
+
+ addInnerElements(newChild, element, idMap);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridViewRule.java
new file mode 100644
index 000000000..b82f391b4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridViewRule.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_NUM_COLUMNS;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+
+/**
+ * An {@link IViewRule} for android.widget.GridView
+ */
+public class GridViewRule extends BaseViewRule {
+
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, getFillParentValueName());
+ node.setAttribute(ANDROID_URI, ATTR_NUM_COLUMNS, "3"); //$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/HorizontalScrollViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/HorizontalScrollViewRule.java
new file mode 100644
index 000000000..722949051
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/HorizontalScrollViewRule.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_ORIENTATION;
+import static com.android.SdkConstants.FQCN_LINEAR_LAYOUT;
+import static com.android.SdkConstants.VALUE_HORIZONTAL;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.IGraphics;
+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.Point;
+import com.android.ide.common.api.Rect;
+
+/**
+ * An {@link IViewRule} for android.widget.HorizontalScrollView.
+ */
+public class HorizontalScrollViewRule extends FrameLayoutRule {
+
+ @Override
+ public void onChildInserted(@NonNull INode child, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onChildInserted(child, parent, insertType);
+
+ // The child of the ScrollView should fill in both directions
+ String fillParent = getFillParentValueName();
+ child.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent);
+ child.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, fillParent);
+ }
+
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ if (insertType.isCreate()) {
+ // Insert a horizontal linear layout which is commonly used with horizontal scrollbars
+ // as described by the documentation for HorizontalScrollbars.
+ INode linearLayout = node.appendChild(FQCN_LINEAR_LAYOUT);
+ linearLayout.setAttribute(ANDROID_URI, ATTR_ORIENTATION,
+ VALUE_HORIZONTAL);
+ }
+ }
+
+ @Override
+ public DropFeedback onDropMove(@NonNull INode targetNode, @NonNull IDragElement[] elements,
+ @Nullable DropFeedback feedback, @NonNull Point p) {
+ DropFeedback f = super.onDropMove(targetNode, elements, feedback, p);
+
+ // HorizontalScrollViews only allow a single child
+ if (targetNode.getChildren().length > 0) {
+ f.invalidTarget = true;
+ }
+ return f;
+ }
+
+ @Override
+ protected void drawFeedback(
+ IGraphics gc,
+ INode targetNode,
+ IDragElement[] elements,
+ DropFeedback feedback) {
+ if (targetNode.getChildren().length > 0) {
+ Rect b = targetNode.getBounds();
+ if (b.isValid()) {
+ gc.useStyle(DrawingStyle.DROP_RECIPIENT);
+ gc.drawRect(b);
+ }
+ } else {
+ super.drawFeedback(gc, targetNode, elements, feedback);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/IgnoredLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/IgnoredLayoutRule.java
new file mode 100644
index 000000000..3a65a8601
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/IgnoredLayoutRule.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2010 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;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.INode;
+
+/**
+ * An ignored layout is a layout that should not be treated as a layout by the
+ * visual editor (usually because the widget extends a layout class we recognize
+ * and support, but where the widget is more restrictive in how it manages its
+ * children so we don't want to expose the normal configuration options).
+ * <p>
+ * For example, the ZoomControls widget is not user-configurable as a
+ * LinearLayout even though it extends it. Our ZoomControls rule is therefore a
+ * subclass of this {@link IgnoredLayoutRule} class.
+ */
+public abstract class IgnoredLayoutRule extends BaseLayoutRule {
+ @Override
+ public DropFeedback onDropEnter(@NonNull INode targetNode, @Nullable Object targetView,
+ @Nullable IDragElement[] elements) {
+ // Do nothing; this layout rule corresponds to a layout that
+ // should not be handled as a layout by the visual editor - usually
+ // because some widget is extending a layout for implementation purposes
+ // but does not want to expose configurability of the base layout in the
+ // editor.
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ImageButtonRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ImageButtonRule.java
new file mode 100644
index 000000000..990795091
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ImageButtonRule.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_SRC;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+
+/**
+ * An {@link IViewRule} for android.widget.ImageButton.
+ */
+public class ImageButtonRule extends BaseViewRule {
+
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ // When dropping an include tag, ask the user which layout to include.
+ if (insertType == InsertType.CREATE) { // NOT InsertType.CREATE_PREVIEW
+ String src = mRulesEngine.displayResourceInput("drawable", ""); //$NON-NLS-1$ //$NON-NLS-2$
+ if (src != null) {
+ node.editXml("Set Image",
+ new PropertySettingNodeHandler(ANDROID_URI, ATTR_SRC,
+ src.length() > 0 ? src : null));
+ return;
+ }
+ }
+
+ // Fallback if dismissed or during previews etc
+ if (insertType.isCreate()) {
+ node.setAttribute(ANDROID_URI, ATTR_SRC, getSampleImageSrc());
+ }
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ImageViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ImageViewRule.java
new file mode 100644
index 000000000..bc0184c4f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ImageViewRule.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_SRC;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+
+/**
+ * An {@link IViewRule} for android.widget.ImageViewRule.
+ */
+public class ImageViewRule extends BaseViewRule {
+
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ // When dropping an include tag, ask the user which layout to include.
+ if (insertType == InsertType.CREATE) { // NOT InsertType.CREATE_PREVIEW
+ String src = mRulesEngine.displayResourceInput("drawable", ""); //$NON-NLS-1$ //$NON-NLS-2$
+ if (src != null) {
+ node.editXml("Set Image",
+ new PropertySettingNodeHandler(ANDROID_URI, ATTR_SRC,
+ src.length() > 0 ? src : null));
+ return;
+ } else {
+ // Remove the view; the insertion was canceled
+ parent.removeChild(node);
+ }
+ }
+
+ // Fallback if dismissed or during previews etc
+ if (insertType.isCreate()) {
+ node.setAttribute(ANDROID_URI, ATTR_SRC, getSampleImageSrc());
+ }
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/IncludeRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/IncludeRule.java
new file mode 100644
index 000000000..fcb1a6dac
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/IncludeRule.java
@@ -0,0 +1,46 @@
+/*
+ * 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;
+
+import static com.android.SdkConstants.ATTR_LAYOUT;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+
+/**
+ * An {@link IViewRule} for the special XML {@code <include>} tag.
+ */
+public class IncludeRule extends BaseViewRule {
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ // When dropping an include tag, ask the user which layout to include.
+ if (insertType == InsertType.CREATE) { // NOT InsertType.CREATE_PREVIEW
+ String include = mRulesEngine.displayIncludeSourceInput();
+ if (include != null) {
+ node.editXml("Include Layout",
+ // Note -- the layout attribute is NOT in the Android namespace!
+ new PropertySettingNodeHandler(null, ATTR_LAYOUT,
+ include.length() > 0 ? include : null));
+ } else {
+ // Remove the view; the insertion was canceled
+ parent.removeChild(node);
+ }
+ }
+ }
+}
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
new file mode 100644
index 000000000..610fe5d8b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java
@@ -0,0 +1,1092 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_BASELINE_ALIGNED;
+import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_ORIENTATION;
+import static com.android.SdkConstants.ATTR_WEIGHT_SUM;
+import static com.android.SdkConstants.VALUE_1;
+import static com.android.SdkConstants.VALUE_HORIZONTAL;
+import static com.android.SdkConstants.VALUE_VERTICAL;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+import static com.android.SdkConstants.VALUE_ZERO_DP;
+import static com.android.ide.eclipse.adt.AdtUtils.formatFloatAttribute;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+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;
+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.IViewMetadata;
+import com.android.ide.common.api.IViewMetadata.FillPreference;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+import com.android.ide.common.api.Point;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.api.RuleAction.Choices;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+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
+ * classes.
+ */
+public class LinearLayoutRule extends BaseLayoutRule {
+ private static final String ACTION_ORIENTATION = "_orientation"; //$NON-NLS-1$
+ private static final String ACTION_WEIGHT = "_weight"; //$NON-NLS-1$
+ private static final String ACTION_DISTRIBUTE = "_distribute"; //$NON-NLS-1$
+ private static final String ACTION_BASELINE = "_baseline"; //$NON-NLS-1$
+ private static final String ACTION_CLEAR = "_clear"; //$NON-NLS-1$
+ private static final String ACTION_DOMINATE = "_dominate"; //$NON-NLS-1$
+
+ private static final URL ICON_HORIZONTAL =
+ LinearLayoutRule.class.getResource("hlinear.png"); //$NON-NLS-1$
+ private static final URL ICON_VERTICAL =
+ LinearLayoutRule.class.getResource("vlinear.png"); //$NON-NLS-1$
+ private static final URL ICON_WEIGHTS =
+ LinearLayoutRule.class.getResource("weights.png"); //$NON-NLS-1$
+ private static final URL ICON_DISTRIBUTE =
+ LinearLayoutRule.class.getResource("distribute.png"); //$NON-NLS-1$
+ private static final URL ICON_BASELINE =
+ LinearLayoutRule.class.getResource("baseline.png"); //$NON-NLS-1$
+ private static final URL ICON_CLEAR_WEIGHTS =
+ LinearLayoutRule.class.getResource("clearweights.png"); //$NON-NLS-1$
+ private static final URL ICON_DOMINATE =
+ LinearLayoutRule.class.getResource("allweight.png"); //$NON-NLS-1$
+
+ /**
+ * Returns the current orientation, regardless of whether it has been defined in XML
+ *
+ * @param node The LinearLayout to look up the orientation for
+ * @return "horizontal" or "vertical" depending on the current orientation of the
+ * linear layout
+ */
+ private String getCurrentOrientation(final INode node) {
+ String orientation = node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION);
+ if (orientation == null || orientation.length() == 0) {
+ orientation = VALUE_HORIZONTAL;
+ }
+ return orientation;
+ }
+
+ /**
+ * Returns true if the given node represents a vertical linear layout.
+ * @param node the node to check layout orientation for
+ * @return true if the layout is in vertical mode, otherwise false
+ */
+ protected boolean isVertical(INode node) {
+ // Horizontal is the default, so if no value is specified it is horizontal.
+ return VALUE_VERTICAL.equals(node.getStringAttr(ANDROID_URI,
+ ATTR_ORIENTATION));
+ }
+
+ /**
+ * Returns true if this LinearLayout supports switching orientation.
+ *
+ * @return true if this layout supports orientations
+ */
+ protected boolean supportsOrientation() {
+ return true;
+ }
+
+ @Override
+ public void addLayoutActions(
+ @NonNull List<RuleAction> actions,
+ final @NonNull INode parentNode,
+ final @NonNull List<? extends INode> children) {
+ super.addLayoutActions(actions, parentNode, children);
+ if (supportsOrientation()) {
+ Choices action = RuleAction.createChoices(
+ ACTION_ORIENTATION, "Orientation", //$NON-NLS-1$
+ new PropertyCallback(Collections.singletonList(parentNode),
+ "Change LinearLayout Orientation",
+ ANDROID_URI, ATTR_ORIENTATION),
+ Arrays.<String>asList("Set Horizontal Orientation","Set Vertical Orientation"),
+ Arrays.<URL>asList(ICON_HORIZONTAL, ICON_VERTICAL),
+ Arrays.<String>asList("horizontal", "vertical"),
+ getCurrentOrientation(parentNode),
+ null /* icon */,
+ -10,
+ false /* supportsMultipleNodes */
+ );
+ action.setRadio(true);
+ actions.add(action);
+ }
+ if (!isVertical(parentNode)) {
+ String current = parentNode.getStringAttr(ANDROID_URI, ATTR_BASELINE_ALIGNED);
+ boolean isAligned = current == null || Boolean.valueOf(current);
+ actions.add(RuleAction.createToggle(ACTION_BASELINE, "Toggle Baseline Alignment",
+ isAligned,
+ new PropertyCallback(Collections.singletonList(parentNode),
+ "Change Baseline Alignment",
+ ANDROID_URI, ATTR_BASELINE_ALIGNED), // TODO: Also set index?
+ ICON_BASELINE, 38, false));
+ }
+
+ // Gravity
+ if (children != null && children.size() > 0) {
+ actions.add(RuleAction.createSeparator(35));
+
+ // Margins
+ actions.add(createMarginAction(parentNode, children));
+
+ // Gravity
+ actions.add(createGravityAction(children, ATTR_LAYOUT_GRAVITY));
+
+ // Weights
+ IMenuCallback actionCallback = new IMenuCallback() {
+ @Override
+ public void action(
+ final @NonNull RuleAction action,
+ @NonNull List<? extends INode> selectedNodes,
+ final @Nullable String valueId,
+ final @Nullable Boolean newValue) {
+ parentNode.editXml("Change Weight", new INodeHandler() {
+ @Override
+ public void handle(@NonNull INode n) {
+ String id = action.getId();
+ if (id.equals(ACTION_WEIGHT)) {
+ String weight =
+ children.get(0).getStringAttr(ANDROID_URI, ATTR_LAYOUT_WEIGHT);
+ if (weight == null || weight.length() == 0) {
+ weight = "0.0"; //$NON-NLS-1$
+ }
+ weight = mRulesEngine.displayInput("Enter Weight Value:", weight,
+ null);
+ if (weight != null) {
+ if (weight.isEmpty()) {
+ weight = null; // remove attribute
+ }
+ for (INode child : children) {
+ child.setAttribute(ANDROID_URI,
+ ATTR_LAYOUT_WEIGHT, weight);
+ }
+ }
+ } else if (id.equals(ACTION_DISTRIBUTE)) {
+ distributeWeights(parentNode, parentNode.getChildren());
+ } else if (id.equals(ACTION_CLEAR)) {
+ clearWeights(parentNode);
+ } else if (id.equals(ACTION_CLEAR) || id.equals(ACTION_DOMINATE)) {
+ clearWeights(parentNode);
+ distributeWeights(parentNode,
+ children.toArray(new INode[children.size()]));
+ } else {
+ assert id.equals(ACTION_BASELINE);
+ }
+ }
+ });
+ }
+ };
+ actions.add(RuleAction.createSeparator(50));
+ actions.add(RuleAction.createAction(ACTION_DISTRIBUTE, "Distribute Weights Evenly",
+ actionCallback, ICON_DISTRIBUTE, 60, false /*supportsMultipleNodes*/));
+ actions.add(RuleAction.createAction(ACTION_DOMINATE, "Assign All Weight",
+ actionCallback, ICON_DOMINATE, 70, false));
+ actions.add(RuleAction.createAction(ACTION_WEIGHT, "Change Layout Weight",
+ actionCallback, ICON_WEIGHTS, 80, false));
+ actions.add(RuleAction.createAction(ACTION_CLEAR, "Clear All Weights",
+ actionCallback, ICON_CLEAR_WEIGHTS, 90, false));
+ }
+ }
+
+ private void distributeWeights(INode parentNode, INode[] targets) {
+ // Any XML to get weight sum?
+ String weightSum = parentNode.getStringAttr(ANDROID_URI,
+ ATTR_WEIGHT_SUM);
+ double sum = -1.0;
+ if (weightSum != null) {
+ // Distribute
+ try {
+ sum = Double.parseDouble(weightSum);
+ } catch (NumberFormatException nfe) {
+ // Just keep using the default
+ }
+ }
+ int numTargets = targets.length;
+ double share;
+ if (sum <= 0.0) {
+ // The sum will be computed from the children, so just
+ // use arbitrary amount
+ share = 1.0;
+ } else {
+ share = sum / numTargets;
+ }
+ String value = formatFloatAttribute((float) share);
+ String sizeAttribute = isVertical(parentNode) ?
+ ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH;
+ for (INode target : targets) {
+ target.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, value);
+ // Also set the width/height to 0dp to ensure actual equal
+ // size (without this, only the remaining space is
+ // distributed)
+ if (VALUE_WRAP_CONTENT.equals(target.getStringAttr(ANDROID_URI, sizeAttribute))) {
+ target.setAttribute(ANDROID_URI, sizeAttribute, VALUE_ZERO_DP);
+ }
+ }
+ }
+
+ private void clearWeights(INode parentNode) {
+ // Clear attributes
+ String sizeAttribute = isVertical(parentNode)
+ ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH;
+ for (INode target : parentNode.getChildren()) {
+ target.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null);
+ String size = target.getStringAttr(ANDROID_URI, sizeAttribute);
+ if (size != null && size.startsWith("0")) { //$NON-NLS-1$
+ target.setAttribute(ANDROID_URI, sizeAttribute, VALUE_WRAP_CONTENT);
+ }
+ }
+ }
+
+ // ==== Drag'n'drop support ====
+
+ @Override
+ public DropFeedback onDropEnter(final @NonNull INode targetNode, @Nullable Object targetView,
+ final @Nullable IDragElement[] elements) {
+
+ if (elements.length == 0) {
+ return null;
+ }
+
+ Rect bn = targetNode.getBounds();
+ if (!bn.isValid()) {
+ return null;
+ }
+
+ boolean isVertical = isVertical(targetNode);
+
+ // Prepare a list of insertion points: X coords for horizontal, Y for
+ // vertical.
+ List<MatchPos> indexes = new ArrayList<MatchPos>();
+
+ int last = isVertical ? bn.y : bn.x;
+ int pos = 0;
+ boolean lastDragged = false;
+ int selfPos = -1;
+ for (INode it : targetNode.getChildren()) {
+ Rect bc = it.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 (element.isSame(it)) {
+ isDragged = true;
+ break;
+ }
+ }
+
+ // We don't want to insert drag positions before or after the
+ // element that is itself being dragged. However, we -do- want
+ // to insert a match position here, at the center, such that
+ // when you drag near its current position we show a match right
+ // where it's already positioned.
+ if (isDragged) {
+ int v = isVertical ? bc.y + (bc.h / 2) : bc.x + (bc.w / 2);
+ selfPos = pos;
+ indexes.add(new MatchPos(v, pos++));
+ } else if (lastDragged) {
+ // Even though we don't want to insert a match below, we
+ // need to increment the index counter such that subsequent
+ // lines know their correct index in the child list.
+ pos++;
+ } else {
+ // Add an insertion point between the last point and the
+ // start of this child
+ int v = isVertical ? bc.y : bc.x;
+ v = (last + v) / 2;
+ indexes.add(new MatchPos(v, pos++));
+ }
+
+ last = isVertical ? (bc.y + bc.h) : (bc.x + bc.w);
+ lastDragged = isDragged;
+ } else {
+ // We still have to count this position even if it has no bounds, or
+ // subsequent children will be inserted at the wrong place
+ pos++;
+ }
+ }
+
+ // Finally add an insert position after all the children - unless of
+ // course we happened to be dragging the last element
+ if (!lastDragged) {
+ int v = last + 1;
+ indexes.add(new MatchPos(v, pos));
+ }
+
+ int posCount = targetNode.getChildren().length + 1;
+ return new DropFeedback(new LinearDropData(indexes, posCount, isVertical, selfPos),
+ new IFeedbackPainter() {
+
+ @Override
+ public void paint(@NonNull IGraphics gc, @NonNull INode node,
+ @NonNull DropFeedback feedback) {
+ // Paint callback for the LinearLayout. This is called
+ // by the canvas when a draw is needed.
+ drawFeedback(gc, node, elements, feedback);
+ }
+ });
+ }
+
+ void drawFeedback(IGraphics gc, INode node, IDragElement[] elements, DropFeedback feedback) {
+ Rect b = node.getBounds();
+ if (!b.isValid()) {
+ return;
+ }
+
+ // Highlight the receiver
+ gc.useStyle(DrawingStyle.DROP_RECIPIENT);
+ gc.drawRect(b);
+
+ gc.useStyle(DrawingStyle.DROP_ZONE);
+
+ LinearDropData data = (LinearDropData) feedback.userData;
+ boolean isVertical = data.isVertical();
+ int selfPos = data.getSelfPos();
+
+ for (MatchPos it : data.getIndexes()) {
+ int i = it.getDistance();
+ int pos = it.getPosition();
+ // Don't show insert drop zones for "self"-index since that one goes
+ // right through the center of the widget rather than in a sibling
+ // position
+ if (pos != selfPos) {
+ if (isVertical) {
+ // draw horizontal lines
+ gc.drawLine(b.x, i, b.x + b.w, i);
+ } else {
+ // draw vertical lines
+ gc.drawLine(i, b.y, i, b.y + b.h);
+ }
+ }
+ }
+
+ Integer currX = data.getCurrX();
+ Integer currY = data.getCurrY();
+
+ if (currX != null && currY != null) {
+ gc.useStyle(DrawingStyle.DROP_ZONE_ACTIVE);
+
+ int x = currX;
+ int y = currY;
+
+ Rect be = elements[0].getBounds();
+
+ // Draw a clear line at the closest drop zone (unless we're over the
+ // dragged element itself)
+ if (data.getInsertPos() != selfPos || selfPos == -1) {
+ gc.useStyle(DrawingStyle.DROP_PREVIEW);
+ if (data.getWidth() != null) {
+ int width = data.getWidth();
+ int fromX = x - width / 2;
+ int toX = x + width / 2;
+ gc.drawLine(fromX, y, toX, y);
+ } else if (data.getHeight() != null) {
+ int height = data.getHeight();
+ int fromY = y - height / 2;
+ int toY = y + height / 2;
+ gc.drawLine(x, fromY, x, toY);
+ }
+ }
+
+ if (be.isValid()) {
+ boolean isLast = data.isLastPosition();
+
+ // At least the first element has a bound. Draw rectangles for
+ // all dropped elements with valid bounds, offset at the drop
+ // point.
+ int offsetX;
+ int offsetY;
+ if (isVertical) {
+ offsetX = b.x - be.x;
+ offsetY = currY - be.y - (isLast ? 0 : (be.h / 2));
+
+ } else {
+ offsetX = currX - be.x - (isLast ? 0 : (be.w / 2));
+ offsetY = b.y - be.y;
+ }
+
+ gc.useStyle(DrawingStyle.DROP_PREVIEW);
+ for (IDragElement element : elements) {
+ Rect bounds = element.getBounds();
+ if (bounds.isValid() && (bounds.w > b.w || bounds.h > b.h) &&
+ node.getChildren().length == 0) {
+ // The bounds of the child does not fully fit inside the target.
+ // Limit the bounds to the layout bounds (but only when there
+ // are no children, since otherwise positioning around the existing
+ // children gets difficult)
+ final int px, py, pw, ph;
+ if (bounds.w > b.w) {
+ px = b.x;
+ pw = b.w;
+ } else {
+ px = bounds.x + offsetX;
+ pw = bounds.w;
+ }
+ if (bounds.h > b.h) {
+ py = b.y;
+ ph = b.h;
+ } else {
+ py = bounds.y + offsetY;
+ ph = bounds.h;
+ }
+ Rect within = new Rect(px, py, pw, ph);
+ gc.drawRect(within);
+ } else {
+ drawElement(gc, element, offsetX, offsetY);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public DropFeedback onDropMove(@NonNull INode targetNode, @NonNull IDragElement[] elements,
+ @Nullable DropFeedback feedback, @NonNull Point p) {
+ Rect b = targetNode.getBounds();
+ if (!b.isValid()) {
+ return feedback;
+ }
+
+ LinearDropData data = (LinearDropData) feedback.userData;
+ boolean isVertical = data.isVertical();
+
+ int bestDist = Integer.MAX_VALUE;
+ int bestIndex = Integer.MIN_VALUE;
+ Integer bestPos = null;
+
+ for (MatchPos index : data.getIndexes()) {
+ int i = index.getDistance();
+ int pos = index.getPosition();
+ int dist = (isVertical ? p.y : p.x) - i;
+ if (dist < 0)
+ dist = -dist;
+ if (dist < bestDist) {
+ bestDist = dist;
+ bestIndex = i;
+ bestPos = pos;
+ if (bestDist <= 0)
+ break;
+ }
+ }
+
+ if (bestIndex != Integer.MIN_VALUE) {
+ Integer oldX = data.getCurrX();
+ Integer oldY = data.getCurrY();
+
+ if (isVertical) {
+ data.setCurrX(b.x + b.w / 2);
+ data.setCurrY(bestIndex);
+ data.setWidth(b.w);
+ data.setHeight(null);
+ } else {
+ data.setCurrX(bestIndex);
+ data.setCurrY(b.y + b.h / 2);
+ data.setWidth(null);
+ data.setHeight(b.h);
+ }
+
+ data.setInsertPos(bestPos);
+
+ feedback.requestPaint = !equals(oldX, data.getCurrX())
+ || !equals(oldY, data.getCurrY());
+ }
+
+ return feedback;
+ }
+
+ private static boolean equals(Integer i1, Integer i2) {
+ if (i1 == i2) {
+ return true;
+ } else if (i1 != null) {
+ return i1.equals(i2);
+ } else {
+ // We know i2 != null
+ return i2.equals(i1);
+ }
+ }
+
+ @Override
+ public void onDropLeave(@NonNull INode targetNode, @NonNull IDragElement[] elements,
+ @Nullable DropFeedback feedback) {
+ // ignore
+ }
+
+ @Override
+ public void onDropped(final @NonNull INode targetNode, final @NonNull IDragElement[] elements,
+ final @Nullable DropFeedback feedback, final @NonNull Point p) {
+
+ LinearDropData data = (LinearDropData) feedback.userData;
+ final int initialInsertPos = data.getInsertPos();
+ insertAt(targetNode, elements, feedback.isCopy || !feedback.sameCanvas, initialInsertPos);
+ }
+
+ @Override
+ public void onChildInserted(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ if (insertType == InsertType.MOVE_WITHIN) {
+ // Don't adjust widths/heights/weights when just moving within a single
+ // LinearLayout
+ return;
+ }
+
+ // Attempt to set fill-properties on newly added views such that for example,
+ // in a vertical layout, a text field defaults to filling horizontally, but not
+ // vertically.
+ String fqcn = node.getFqcn();
+ IViewMetadata metadata = mRulesEngine.getMetadata(fqcn);
+ if (metadata != null) {
+ boolean vertical = isVertical(parent);
+ FillPreference fill = metadata.getFillPreference();
+ String fillParent = getFillParentValueName();
+ if (fill.fillHorizontally(vertical)) {
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent);
+ } else if (!vertical && fill == FillPreference.WIDTH_IN_VERTICAL) {
+ // In a horizontal layout, make views that would fill horizontally in a
+ // vertical layout have a non-zero weight instead. This will make the item
+ // fill but only enough to allow other views to be shown as well.
+ // (However, for drags within the same layout we do not touch
+ // the weight, since it might already have been tweaked to a particular
+ // value)
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, VALUE_1);
+ }
+ if (fill.fillVertically(vertical)) {
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, fillParent);
+ }
+ }
+
+ // If you insert into a layout that already is using layout weights,
+ // and all the layout weights are the same (nonzero) value, then use
+ // the same weight for this new layout as well. Also duplicate the 0dip/0px/0dp
+ // sizes, if used.
+ boolean duplicateWeight = true;
+ boolean duplicate0dip = true;
+ String sameWeight = null;
+ String sizeAttribute = isVertical(parent) ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH;
+ for (INode target : parent.getChildren()) {
+ if (target == node) {
+ continue;
+ }
+ String weight = target.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WEIGHT);
+ if (weight == null || weight.length() == 0) {
+ duplicateWeight = false;
+ break;
+ } else if (sameWeight != null && !sameWeight.equals(weight)) {
+ duplicateWeight = false;
+ } else {
+ sameWeight = weight;
+ }
+ String size = target.getStringAttr(ANDROID_URI, sizeAttribute);
+ if (size != null && !size.startsWith("0")) { //$NON-NLS-1$
+ duplicate0dip = false;
+ break;
+ }
+ }
+ if (duplicateWeight && sameWeight != null) {
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, sameWeight);
+ if (duplicate0dip) {
+ node.setAttribute(ANDROID_URI, sizeAttribute, VALUE_ZERO_DP);
+ }
+ }
+ }
+
+ /** A possible match position */
+ private static class MatchPos {
+ /** The pixel distance */
+ private int mDistance;
+ /** The position among siblings */
+ private int mPosition;
+
+ public MatchPos(int distance, int position) {
+ mDistance = distance;
+ mPosition = position;
+ }
+
+ @Override
+ public String toString() {
+ return "MatchPos [distance=" + mDistance //$NON-NLS-1$
+ + ", position=" + mPosition //$NON-NLS-1$
+ + "]"; //$NON-NLS-1$
+ }
+
+ private int getDistance() {
+ return mDistance;
+ }
+
+ private int getPosition() {
+ return mPosition;
+ }
+ }
+
+ private static class LinearDropData {
+ /** Vertical layout? */
+ private final boolean mVertical;
+
+ /** Insert points (pixels + index) */
+ private final List<MatchPos> mIndexes;
+
+ /** Number of insert positions in the target node */
+ private final int mNumPositions;
+
+ /** Current marker X position */
+ private Integer mCurrX;
+
+ /** Current marker Y position */
+ private Integer mCurrY;
+
+ /** Position of the dragged element in this layout (or
+ -1 if the dragged element is from elsewhere) */
+ private final int mSelfPos;
+
+ /** Current drop insert index (-1 for "at the end") */
+ private int mInsertPos = -1;
+
+ /** width of match line if it's a horizontal one */
+ private Integer mWidth;
+
+ /** height of match line if it's a vertical one */
+ private Integer mHeight;
+
+ public LinearDropData(List<MatchPos> indexes, int numPositions,
+ boolean isVertical, int selfPos) {
+ mIndexes = indexes;
+ mNumPositions = numPositions;
+ mVertical = isVertical;
+ mSelfPos = selfPos;
+ }
+
+ @Override
+ public String toString() {
+ return "LinearDropData [currX=" + mCurrX //$NON-NLS-1$
+ + ", currY=" + mCurrY //$NON-NLS-1$
+ + ", height=" + mHeight //$NON-NLS-1$
+ + ", indexes=" + mIndexes //$NON-NLS-1$
+ + ", insertPos=" + mInsertPos //$NON-NLS-1$
+ + ", isVertical=" + mVertical //$NON-NLS-1$
+ + ", selfPos=" + mSelfPos //$NON-NLS-1$
+ + ", width=" + mWidth //$NON-NLS-1$
+ + "]"; //$NON-NLS-1$
+ }
+
+ private boolean isVertical() {
+ return mVertical;
+ }
+
+ private void setCurrX(Integer currX) {
+ mCurrX = currX;
+ }
+
+ private Integer getCurrX() {
+ return mCurrX;
+ }
+
+ private void setCurrY(Integer currY) {
+ mCurrY = currY;
+ }
+
+ private Integer getCurrY() {
+ return mCurrY;
+ }
+
+ private int getSelfPos() {
+ return mSelfPos;
+ }
+
+ private void setInsertPos(int insertPos) {
+ mInsertPos = insertPos;
+ }
+
+ private int getInsertPos() {
+ return mInsertPos;
+ }
+
+ private List<MatchPos> getIndexes() {
+ return mIndexes;
+ }
+
+ private void setWidth(Integer width) {
+ mWidth = width;
+ }
+
+ private Integer getWidth() {
+ return mWidth;
+ }
+
+ private void setHeight(Integer height) {
+ mHeight = height;
+ }
+
+ private Integer getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Returns true if we are inserting into the last position
+ *
+ * @return true if we are inserting into the last position
+ */
+ public boolean isLastPosition() {
+ return mInsertPos == mNumPositions - 1;
+ }
+ }
+
+ /** Custom resize state used during linear layout resizing */
+ private class LinearResizeState extends ResizeState {
+ /** Whether the node should be assigned a new weight */
+ public boolean useWeight;
+ /** Weight sum to be applied to the parent */
+ private float mNewWeightSum;
+ /** The weight to be set on the node (provided {@link #useWeight} is true) */
+ private float mWeight;
+ /** Map from nodes to preferred bounds of nodes where the weights have been cleared */
+ public final Map<INode, Rect> unweightedSizes;
+ /** Total required size required by the siblings <b>without</b> weights */
+ public int totalLength;
+ /** List of nodes which should have their weights cleared */
+ public List<INode> mClearWeights;
+
+ private LinearResizeState(BaseLayoutRule rule, INode layout, Object layoutView,
+ INode node) {
+ super(rule, layout, layoutView, node);
+
+ unweightedSizes = mRulesEngine.measureChildren(layout,
+ new IClientRulesEngine.AttributeFilter() {
+ @Override
+ public String getAttribute(@NonNull INode n, @Nullable String namespace,
+ @NonNull 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;
+ }
+ });
+
+ // Compute total required size required by the siblings *without* weights
+ totalLength = 0;
+ final boolean isVertical = isVertical(layout);
+ for (Map.Entry<INode, Rect> entry : unweightedSizes.entrySet()) {
+ Rect preferredSize = entry.getValue();
+ if (isVertical) {
+ totalLength += preferredSize.h;
+ } else {
+ totalLength += preferredSize.w;
+ }
+ }
+ }
+
+ /** Resets the computed state */
+ void reset() {
+ mNewWeightSum = -1;
+ useWeight = false;
+ mClearWeights = null;
+ }
+
+ /** Sets a weight to be applied to the node */
+ void setWeight(float weight) {
+ useWeight = true;
+ mWeight = weight;
+ }
+
+ /** Sets a weight sum to be applied to the parent layout */
+ void setWeightSum(float weightSum) {
+ mNewWeightSum = weightSum;
+ }
+
+ /** Marks that the given node should be cleared when applying the new size */
+ void clearWeight(INode n) {
+ if (mClearWeights == null) {
+ mClearWeights = new ArrayList<INode>();
+ }
+ mClearWeights.add(n);
+ }
+
+ /** Applies the state to the nodes */
+ public void apply() {
+ assert useWeight;
+
+ String value = mWeight > 0 ? formatFloatAttribute(mWeight) : null;
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, value);
+
+ if (mClearWeights != null) {
+ for (INode n : mClearWeights) {
+ if (getWeight(n) > 0.0f) {
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null);
+ }
+ }
+ }
+
+ if (mNewWeightSum > 0.0) {
+ layout.setAttribute(ANDROID_URI, ATTR_WEIGHT_SUM,
+ formatFloatAttribute(mNewWeightSum));
+ }
+ }
+ }
+
+ @Override
+ protected ResizeState createResizeState(INode layout, Object layoutView, INode node) {
+ return new LinearResizeState(this, layout, layoutView, node);
+ }
+
+ protected void updateResizeState(LinearResizeState resizeState, final INode node, INode layout,
+ Rect oldBounds, Rect newBounds, SegmentType horizontalEdge,
+ SegmentType verticalEdge) {
+ // Update the resize state.
+ // This method attempts to compute a new layout weight to be used in the direction
+ // of the linear layout. If the superclass has already determined that we can snap to
+ // a wrap_content or match_parent boundary, we prefer that. Otherwise, we attempt to
+ // compute a layout weight - which can fail if the size is too big (not enough room),
+ // or if the size is too small (smaller than the natural width of the node), and so on.
+ // In that case this method just aborts, which will leave the resize state object
+ // in such a state that it will call the superclass to resize instead, which will fall
+ // back to device independent pixel sizing.
+ resizeState.reset();
+
+ if (oldBounds.equals(newBounds)) {
+ return;
+ }
+
+ // If we're setting the width/height to wrap_content/match_parent in the dimension of the
+ // linear layout, then just apply wrap_content and clear weights.
+ boolean isVertical = isVertical(layout);
+ if (!isVertical && verticalEdge != null) {
+ if (resizeState.wrapWidth || resizeState.fillWidth) {
+ resizeState.clearWeight(node);
+ return;
+ }
+ if (newBounds.w == oldBounds.w) {
+ return;
+ }
+ }
+
+ if (isVertical && horizontalEdge != null) {
+ if (resizeState.wrapHeight || resizeState.fillHeight) {
+ resizeState.clearWeight(node);
+ return;
+ }
+ if (newBounds.h == oldBounds.h) {
+ return;
+ }
+ }
+
+ // Compute weight sum
+ float sum = getWeightSum(layout);
+ if (sum <= 0.0f) {
+ sum = 1.0f;
+ resizeState.setWeightSum(sum);
+ }
+
+ // If the new size of the node is smaller than its preferred/wrap_content size,
+ // then we cannot use weights to size it; switch to pixel-based sizing instead
+ Map<INode, Rect> sizes = resizeState.unweightedSizes;
+ Rect nodePreferredSize = sizes.get(node);
+ if (nodePreferredSize != null) {
+ if (horizontalEdge != null && newBounds.h < nodePreferredSize.h ||
+ verticalEdge != null && newBounds.w < nodePreferredSize.w) {
+ return;
+ }
+ }
+
+ Rect layoutBounds = layout.getBounds();
+ int remaining = (isVertical ? layoutBounds.h : layoutBounds.w) - resizeState.totalLength;
+ Rect nodeBounds = sizes.get(node);
+ if (nodeBounds == null) {
+ return;
+ }
+
+ 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;
+ resizeState.wrapHeight = true;
+ }
+ } 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;
+ resizeState.wrapWidth = true;
+ }
+ }
+ if (missing > 0) {
+ // (weight / weightSum) * remaining = missing, so
+ // weight = missing * weightSum / remaining
+ float weight = missing * sum / remaining;
+ resizeState.setWeight(weight);
+ }
+ }
+ }
+
+ /**
+ * {@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 state, final INode node, INode layout,
+ Rect oldBounds, Rect newBounds, SegmentType horizontalEdge,
+ SegmentType verticalEdge) {
+ LinearResizeState resizeState = (LinearResizeState) state;
+ updateResizeState(resizeState, node, layout, oldBounds, newBounds,
+ horizontalEdge, verticalEdge);
+
+ if (resizeState.useWeight) {
+ resizeState.apply();
+
+ // Handle resizing in the opposite dimension of the layout
+ final boolean isVertical = isVertical(layout);
+ if (!isVertical && horizontalEdge != null) {
+ if (newBounds.h != oldBounds.h || resizeState.wrapHeight
+ || resizeState.fillHeight) {
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT,
+ resizeState.getHeightAttribute());
+ }
+ }
+ if (isVertical && verticalEdge != null) {
+ if (newBounds.w != oldBounds.w || resizeState.wrapWidth || resizeState.fillWidth) {
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH,
+ resizeState.getWidthAttribute());
+ }
+ }
+ } else {
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null);
+ super.setNewSizeBounds(resizeState, node, layout, oldBounds, newBounds,
+ horizontalEdge, verticalEdge);
+ }
+ }
+
+ @Override
+ protected String getResizeUpdateMessage(ResizeState state, INode child, INode parent,
+ Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) {
+ LinearResizeState resizeState = (LinearResizeState) state;
+ updateResizeState(resizeState, child, parent, child.getBounds(), newBounds,
+ horizontalEdge, verticalEdge);
+
+ if (resizeState.useWeight) {
+ String weight = formatFloatAttribute(resizeState.mWeight);
+ String dimension = String.format("weight %1$s", weight);
+
+ String width;
+ String height;
+ if (isVertical(parent)) {
+ width = resizeState.getWidthAttribute();
+ height = dimension;
+ } else {
+ width = dimension;
+ height = resizeState.getHeightAttribute();
+ }
+
+ if (horizontalEdge == null) {
+ return width;
+ } else if (verticalEdge == null) {
+ return height;
+ } else {
+ // U+00D7: Unicode for multiplication sign
+ return String.format("%s \u00D7 %s", width, height);
+ }
+ } else {
+ return super.getResizeUpdateMessage(state, child, parent, newBounds,
+ horizontalEdge, verticalEdge);
+ }
+ }
+
+ /**
+ * 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;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ListViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ListViewRule.java
new file mode 100644
index 000000000..70728c81d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ListViewRule.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+
+/**
+ * An {@link IViewRule} for android.widget.ListView and all its derived classes such
+ * as ExpandableListView.
+ * This is the "root" rule, that is used whenever there is not more specific
+ * rule to apply.
+ */
+public class ListViewRule extends AdapterViewRule {
+
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, getFillParentValueName());
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MapViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MapViewRule.java
new file mode 100644
index 000000000..006661e57
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MapViewRule.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+
+/**
+ * An {@link IViewRule} for com.google.android.maps.MapView.
+ * <p>
+ * TODO: This class should be pulled out of the ADT and bundled with the add ons
+ * (not the core jar but an optional tool jar)
+ */
+public class MapViewRule extends BaseViewRule {
+
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ if (insertType.isCreate()) {
+ node.setAttribute(ANDROID_URI, "android:apiKey", //$NON-NLS-1$
+ "Your API key: see " + //$NON-NLS-1$
+ "http://code.google.com/android/add-ons/google-apis/mapkey.html"); //$NON-NLS-1$
+ }
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MergeRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MergeRule.java
new file mode 100644
index 000000000..9cef9c4b3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MergeRule.java
@@ -0,0 +1,38 @@
+/*
+ * 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;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.RuleAction;
+
+import java.util.List;
+
+/**
+ * Drop handler for the {@code <merge>} tag
+ */
+public class MergeRule extends FrameLayoutRule {
+ // The <merge> tag behaves a lot like the FrameLayout; all children are added
+ // on top of each other at (0,0)
+
+ @Override
+ public void addContextMenuActions(@NonNull List<RuleAction> actions,
+ final @NonNull INode selectedNode) {
+ // Deliberately ignore super.getContextMenu(); we don't want to attempt to list
+ // properties for the <merge> tag
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/PropertyCallback.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/PropertyCallback.java
new file mode 100644
index 000000000..da2614eef
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/PropertyCallback.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+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.RuleAction;
+
+import java.util.List;
+
+/**
+ * Convenience implementation of {@link IMenuCallback} which can be used to set a
+ * particular property to the new valueId or newValue passed from the {@link IMenuCallback}
+ */
+public class PropertyCallback implements IMenuCallback {
+ private final List<? extends INode> mTargetNodes;
+ private final String mUndoLabel;
+ private final String mUri;
+ private final String mAttribute;
+
+ /**
+ * Creates a new property callback.
+ *
+ * @param targetNodes the nodes to apply the property to, or null to use the
+ * nodes pass into the
+ * {@link #action(RuleAction, List, String, Boolean)} method.
+ * @param undoLabel the label to use for the undo action
+ * @param uri the attribute URI to apply
+ * @param attribute the attribute name to apply
+ */
+ public PropertyCallback(List<? extends INode> targetNodes, String undoLabel,
+ String uri, String attribute) {
+ super();
+ mTargetNodes = targetNodes;
+ mUndoLabel = undoLabel;
+ mUri = uri;
+ mAttribute = attribute;
+ }
+
+ // ---- Implements IMenuCallback ----
+ @Override
+ public void action(@NonNull RuleAction action, @NonNull List<? extends INode> selectedNodes,
+ final @Nullable String valueId, final @Nullable Boolean newValue) {
+ if (mTargetNodes != null && mTargetNodes.size() > 0) {
+ selectedNodes = mTargetNodes;
+ }
+ if (selectedNodes == null || selectedNodes.size() == 0) {
+ return;
+ }
+ final List<? extends INode> nodes = selectedNodes;
+ selectedNodes.get(0).editXml(mUndoLabel, new INodeHandler() {
+ @Override
+ public void handle(@NonNull INode n) {
+ for (INode targetNode : nodes) {
+ if (valueId != null) {
+ targetNode.setAttribute(mUri, mAttribute, valueId);
+ } else {
+ assert newValue != null;
+ targetNode.setAttribute(mUri, mAttribute, Boolean.toString(newValue));
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/PropertySettingNodeHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/PropertySettingNodeHandler.java
new file mode 100644
index 000000000..13c8842ed
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/PropertySettingNodeHandler.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.INodeHandler;
+
+/**
+ * A convenience implementation of {@link INodeHandler} for setting a given attribute to a
+ * given value on a particular node.
+ */
+class PropertySettingNodeHandler implements INodeHandler {
+ private final String mNamespaceUri;
+ private final String mAttribute;
+ private final String mValue;
+
+ PropertySettingNodeHandler(String namespaceUri, String attribute, String value) {
+ super();
+ mNamespaceUri = namespaceUri;
+ mAttribute = attribute;
+ mValue = value;
+ }
+
+ @Override
+ public void handle(@NonNull INode node) {
+ node.setAttribute(mNamespaceUri, mAttribute, mValue);
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/QuickContactBadgeRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/QuickContactBadgeRule.java
new file mode 100644
index 000000000..0164794d3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/QuickContactBadgeRule.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+
+/**
+ * An {@link IViewRule} for android.widget.QuickContactBadgeRule.
+ */
+public class QuickContactBadgeRule extends ImageViewRule {
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ // Deliberately override onCreate such that we don't populate a default
+ // image; at design time layoutlib will supply the system default contacts
+ // image.
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RadioGroupRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RadioGroupRule.java
new file mode 100644
index 000000000..c9aa20768
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RadioGroupRule.java
@@ -0,0 +1,50 @@
+/*
+ * 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;
+
+import static com.android.SdkConstants.ATTR_CHECKED;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.VALUE_TRUE;
+
+
+import com.android.SdkConstants;
+import static com.android.SdkConstants.ANDROID_URI;
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+
+/**
+ * An {@link IViewRule} for android.widget.RadioGroup which initializes the radio group
+ * with some radio buttons
+ */
+public class RadioGroupRule extends LinearLayoutRule {
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ if (insertType.isCreate()) {
+ for (int i = 0; i < 3; i++) {
+ INode handle = node.appendChild(SdkConstants.FQCN_RADIO_BUTTON);
+ handle.setAttribute(ANDROID_URI, ATTR_ID, String.format("@+id/radio%d", i));
+ if (i == 0) {
+ handle.setAttribute(ANDROID_URI, ATTR_CHECKED, VALUE_TRUE);
+ }
+ }
+ }
+ }
+}
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
new file mode 100644
index 000000000..b4bc86978
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java
@@ -0,0 +1,413 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_GRAVITY;
+import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING;
+import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_IN_PARENT;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
+import static com.android.SdkConstants.VALUE_TRUE;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IDragElement;
+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.Point;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.common.layout.relative.ConstraintPainter;
+import com.android.ide.common.layout.relative.DeletionHandler;
+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.utils.Pair;
+
+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.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 ====
+
+ @Override
+ public List<String> getSelectionHint(@NonNull INode parentNode, @NonNull INode childNode) {
+ List<String> infos = new ArrayList<String>(18);
+ addAttr(ATTR_LAYOUT_ABOVE, childNode, infos);
+ addAttr(ATTR_LAYOUT_BELOW, childNode, infos);
+ addAttr(ATTR_LAYOUT_TO_LEFT_OF, childNode, infos);
+ addAttr(ATTR_LAYOUT_TO_RIGHT_OF, childNode, infos);
+ addAttr(ATTR_LAYOUT_ALIGN_BASELINE, childNode, infos);
+ addAttr(ATTR_LAYOUT_ALIGN_TOP, childNode, infos);
+ addAttr(ATTR_LAYOUT_ALIGN_BOTTOM, childNode, infos);
+ addAttr(ATTR_LAYOUT_ALIGN_LEFT, childNode, infos);
+ addAttr(ATTR_LAYOUT_ALIGN_RIGHT, childNode, infos);
+ addAttr(ATTR_LAYOUT_ALIGN_PARENT_TOP, childNode, infos);
+ addAttr(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, childNode, infos);
+ addAttr(ATTR_LAYOUT_ALIGN_PARENT_LEFT, childNode, infos);
+ addAttr(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, childNode, infos);
+ addAttr(ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING, childNode, infos);
+ addAttr(ATTR_LAYOUT_CENTER_HORIZONTAL, childNode, infos);
+ addAttr(ATTR_LAYOUT_CENTER_IN_PARENT, childNode, infos);
+ addAttr(ATTR_LAYOUT_CENTER_VERTICAL, childNode, infos);
+
+ return infos;
+ }
+
+ private void addAttr(String propertyName, INode childNode, List<String> infos) {
+ String a = childNode.getStringAttr(ANDROID_URI, propertyName);
+ if (a != null && a.length() > 0) {
+ // Display the layout parameters without the leading layout_ prefix
+ // and id references without the @+id/ prefix
+ if (propertyName.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
+ propertyName = propertyName.substring(ATTR_LAYOUT_RESOURCE_PREFIX.length());
+ }
+ a = stripIdPrefix(a);
+ String s = propertyName + ": " + a;
+ infos.add(s);
+ }
+ }
+
+ @Override
+ public void paintSelectionFeedback(@NonNull IGraphics graphics, @NonNull INode parentNode,
+ @NonNull List<? extends INode> childNodes, @Nullable Object view) {
+ super.paintSelectionFeedback(graphics, parentNode, childNodes, view);
+
+ boolean showDependents = true;
+ if (sShowStructure) {
+ childNodes = Arrays.asList(parentNode.getChildren());
+ // Avoid painting twice - both as incoming and outgoing
+ showDependents = false;
+ } else if (!sShowConstraints) {
+ return;
+ }
+
+ ConstraintPainter.paintSelectionFeedback(graphics, parentNode, childNodes, showDependents);
+ }
+
+ // ==== Drag'n'drop support ====
+
+ @Override
+ public DropFeedback onDropEnter(@NonNull INode targetNode, @Nullable Object targetView,
+ @Nullable IDragElement[] elements) {
+ return new DropFeedback(new MoveHandler(targetNode, elements, mRulesEngine),
+ new GuidelinePainter());
+ }
+
+ @Override
+ public DropFeedback onDropMove(@NonNull INode targetNode, @NonNull IDragElement[] elements,
+ @Nullable DropFeedback feedback, @NonNull Point p) {
+ if (elements == null || elements.length == 0 || feedback == null) {
+ return null;
+ }
+
+ 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);
+
+ // Or maybe only do this if the results changed...
+ feedback.requestPaint = true;
+
+ return feedback;
+ }
+
+ @Override
+ public void onDropLeave(@NonNull INode targetNode, @NonNull IDragElement[] elements,
+ @Nullable DropFeedback feedback) {
+ }
+
+ @Override
+ public void onDropped(final @NonNull INode targetNode, final @NonNull IDragElement[] elements,
+ final @Nullable DropFeedback feedback, final @NonNull Point p) {
+ if (feedback == null) {
+ return;
+ }
+
+ final MoveHandler state = (MoveHandler) feedback.userData;
+
+ final Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements,
+ feedback.isCopy || !feedback.sameCanvas);
+
+ targetNode.editXml("Dropped", new INodeHandler() {
+ @Override
+ public void handle(@NonNull INode n) {
+ int index = -1;
+
+ // Remove cycles
+ state.removeCycles();
+
+ // Now write the new elements.
+ INode previous = null;
+ for (IDragElement element : elements) {
+ String fqcn = element.getFqcn();
+
+ // index==-1 means to insert at the end.
+ // Otherwise increment the insertion position.
+ if (index >= 0) {
+ index++;
+ }
+
+ INode newChild = targetNode.insertChildAt(fqcn, index);
+
+ // Copy all the attributes, modifying them as needed.
+ addAttributes(newChild, element, idMap, BaseLayoutRule.DEFAULT_ATTR_FILTER);
+ addInnerElements(newChild, element, idMap);
+
+ if (previous == null) {
+ state.applyConstraints(newChild);
+ previous = newChild;
+ } else {
+ // Arrange the nodes next to each other, depending on which
+ // edge we are attaching to. For example, if attaching to the
+ // top edge, arrange the subsequent nodes in a column below it.
+ //
+ // TODO: Try to do something smarter here where we detect
+ // constraints between the dragged edges, and we preserve these.
+ // We have to do this carefully though because if the
+ // constraints go through some other nodes not part of the
+ // selection, this doesn't work right, and you might be
+ // dragging several connected components, which we'd then
+ // need to stitch together such that they are all visible.
+
+ state.attachPrevious(previous, newChild);
+ previous = newChild;
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onChildInserted(@NonNull INode node, @NonNull INode parent,
+ @NonNull 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$
+ //}
+ }
+
+ @Override
+ public void onRemovingChildren(@NonNull List<INode> deleted, @NonNull INode parent,
+ boolean moved) {
+ super.onRemovingChildren(deleted, parent, moved);
+
+ if (!moved) {
+ DeletionHandler handler = new DeletionHandler(deleted, Collections.<INode>emptyList(),
+ parent);
+ handler.updateConstraints();
+ }
+ }
+
+ // ==== Resize Support ====
+
+ @Override
+ public DropFeedback onResizeBegin(@NonNull INode child, @NonNull INode parent,
+ @Nullable SegmentType horizontalEdgeType, @Nullable SegmentType verticalEdgeType,
+ @Nullable Object childView, @Nullable Object parentView) {
+ ResizeHandler state = new ResizeHandler(parent, child, mRulesEngine,
+ horizontalEdgeType, verticalEdgeType);
+ return new DropFeedback(state, new GuidelinePainter());
+ }
+
+ @Override
+ public void onResizeUpdate(@Nullable DropFeedback feedback, @NonNull INode child,
+ @NonNull INode parent, @NonNull Rect newBounds,
+ int modifierMask) {
+ if (feedback == null) {
+ return;
+ }
+
+ ResizeHandler state = (ResizeHandler) feedback.userData;
+ state.updateResize(feedback, child, newBounds, modifierMask);
+ }
+
+ @Override
+ public void onResizeEnd(@Nullable DropFeedback feedback, @NonNull INode child,
+ @NonNull INode parent, final @NonNull Rect newBounds) {
+ if (feedback == null) {
+ return;
+ }
+ final ResizeHandler state = (ResizeHandler) feedback.userData;
+
+ child.editXml("Resize", new INodeHandler() {
+ @Override
+ public void handle(@NonNull INode n) {
+ state.removeCycles();
+ state.applyConstraints(n);
+ }
+ });
+ }
+
+ // ==== Layout Actions Bar ====
+
+ @Override
+ public void addLayoutActions(
+ @NonNull List<RuleAction> actions,
+ final @NonNull INode parentNode,
+ final @NonNull List<? extends INode> children) {
+ super.addLayoutActions(actions, parentNode, children);
+
+ actions.add(createGravityAction(Collections.<INode>singletonList(parentNode),
+ ATTR_GRAVITY));
+ actions.add(RuleAction.createSeparator(25));
+ actions.add(createMarginAction(parentNode, children));
+
+ IMenuCallback callback = new IMenuCallback() {
+ @Override
+ public void action(@NonNull RuleAction action,
+ @NonNull List<? extends INode> selectedNodes,
+ final @Nullable String valueId,
+ final @Nullable Boolean newValue) {
+ final String id = action.getId();
+ if (id.equals(ACTION_CENTER_VERTICAL)|| id.equals(ACTION_CENTER_HORIZONTAL)) {
+ parentNode.editXml("Center", new INodeHandler() {
+ @Override
+ public void handle(@NonNull 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();
+ }
+
+ });
+ } 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(RuleAction.createSeparator(150));
+ actions.add(RuleAction.createAction(ACTION_CENTER_VERTICAL, "Center Vertically",
+ callback, ICON_CENTER_VERTICALLY, 160, false));
+ actions.add(RuleAction.createAction(ACTION_CENTER_HORIZONTAL, "Center Horizontally",
+ callback, ICON_CENTER_HORIZONTALLY, 170, false));
+ }
+
+ actions.add(RuleAction.createSeparator(80));
+ actions.add(RuleAction.createToggle(ACTION_SHOW_CONSTRAINTS, "Show Constraints",
+ sShowConstraints, callback, ICON_SHOW_CONSTRAINTS, 180, false));
+ actions.add(RuleAction.createToggle(ACTION_SHOW_STRUCTURE, "Show All Relationships",
+ sShowStructure, callback, ICON_SHOW_STRUCTURE, 190, false));
+ }
+
+ 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 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/ResizeState.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ResizeState.java
new file mode 100644
index 000000000..42b9083ad
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ResizeState.java
@@ -0,0 +1,131 @@
+/*
+ * 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;
+
+import static com.android.SdkConstants.VALUE_N_DP;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+
+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;
+
+/** State held during resizing operations */
+class ResizeState {
+ /**
+ * The associated rule
+ */
+ private final BaseLayoutRule mRule;
+
+ /**
+ * The node being resized
+ */
+ public final INode node;
+
+ /**
+ * The layout containing the resized node
+ */
+ public final INode layout;
+
+ /** The proposed resized bounds of the node */
+ public Rect bounds;
+
+ /** The preferred wrap_content bounds of the node */
+ public Rect wrapBounds;
+
+ /** The suggested horizontal fill_parent guideline position */
+ public Segment horizontalFillSegment;
+
+ /** The suggested vertical fill_parent guideline position */
+ public Segment verticalFillSegment;
+
+ /** 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;
+
+ /** Whether the user has snapped to the match_parent width */
+ public boolean fillWidth;
+
+ /** Whether the user has snapped to the match_parent height */
+ public boolean fillHeight;
+
+ /** Custom field for use by subclasses */
+ public Object clientData;
+
+ /** Keyboard mask */
+ public int modifierMask;
+
+ /**
+ * The actual view object for the layout containing the resizing operation,
+ * or null if not known
+ */
+ public Object layoutView;
+
+ /**
+ * Constructs a new {@link ResizeState}
+ *
+ * @param rule the associated rule
+ * @param layout the parent layout containing the resized node
+ * @param layoutView the actual View instance for the layout, or null if not known
+ * @param node the node being resized
+ */
+ ResizeState(BaseLayoutRule rule, INode layout, Object layoutView, INode node) {
+ mRule = rule;
+
+ this.layout = layout;
+ this.node = node;
+ this.layoutView = layoutView;
+ }
+
+ /**
+ * Returns the width attribute to be set to match the new bounds
+ *
+ * @return the width string, never null
+ */
+ public String getWidthAttribute() {
+ if (wrapWidth) {
+ return VALUE_WRAP_CONTENT;
+ } else if (fillWidth) {
+ return mRule.getFillParentValueName();
+ } else {
+ return String.format(VALUE_N_DP, mRule.mRulesEngine.pxToDp(bounds.w));
+ }
+ }
+
+ /**
+ * Returns the height attribute to be set to match the new bounds
+ *
+ * @return the height string, never null
+ */
+ public String getHeightAttribute() {
+ if (wrapHeight) {
+ return VALUE_WRAP_CONTENT;
+ } else if (fillHeight) {
+ return mRule.getFillParentValueName();
+ } else {
+ return String.format(VALUE_N_DP, mRule.mRulesEngine.pxToDp(bounds.h));
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ScrollViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ScrollViewRule.java
new file mode 100644
index 000000000..9f2b4ae6f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ScrollViewRule.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_ORIENTATION;
+import static com.android.SdkConstants.FQCN_LINEAR_LAYOUT;
+import static com.android.SdkConstants.VALUE_VERTICAL;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.IGraphics;
+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.Point;
+import com.android.ide.common.api.Rect;
+
+/**
+ * An {@link IViewRule} for android.widget.ScrollView.
+ */
+public class ScrollViewRule extends FrameLayoutRule {
+
+ @Override
+ public void onChildInserted(@NonNull INode child, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onChildInserted(child, parent, insertType);
+
+ // The child of the ScrollView should fill in both directions
+ String fillParent = getFillParentValueName();
+ child.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent);
+ child.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, fillParent);
+ }
+
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ if (insertType.isCreate()) {
+ // Insert a default linear layout (which will in turn be registered as
+ // a child of this node and the create child method above will set its
+ // fill parent attributes, its id, etc.
+ INode linear = node.appendChild(FQCN_LINEAR_LAYOUT);
+ linear.setAttribute(ANDROID_URI, ATTR_ORIENTATION, VALUE_VERTICAL);
+ }
+ }
+
+ @Override
+ public DropFeedback onDropMove(@NonNull INode targetNode, @NonNull IDragElement[] elements,
+ @Nullable DropFeedback feedback, @NonNull Point p) {
+ DropFeedback f = super.onDropMove(targetNode, elements, feedback, p);
+
+ // ScrollViews only allow a single child
+ if (targetNode.getChildren().length > 0) {
+ f.invalidTarget = true;
+ }
+ return f;
+ }
+
+ @Override
+ protected void drawFeedback(
+ IGraphics gc,
+ INode targetNode,
+ IDragElement[] elements,
+ DropFeedback feedback) {
+ if (targetNode.getChildren().length > 0) {
+ Rect b = targetNode.getBounds();
+ if (b.isValid()) {
+ gc.useStyle(DrawingStyle.DROP_RECIPIENT);
+ gc.drawRect(b);
+ }
+ } else {
+ super.drawFeedback(gc, targetNode, elements, feedback);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/SeekBarRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/SeekBarRule.java
new file mode 100644
index 000000000..b88f8ab25
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/SeekBarRule.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+
+/**
+ * An {@link IViewRule} for android.widget.SeekBar
+ */
+public class SeekBarRule extends BaseViewRule {
+
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ // A SeekBar isn't useful with wrap_content because it packs itself down to
+ // almost no usable width -- so just make it grow in all layouts
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, getFillParentValueName());
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/SlidingDrawerRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/SlidingDrawerRule.java
new file mode 100644
index 000000000..e4267bb10
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/SlidingDrawerRule.java
@@ -0,0 +1,68 @@
+/*
+ * 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;
+
+import static com.android.SdkConstants.ATTR_CONTENT;
+import static com.android.SdkConstants.ATTR_HANDLE;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_TEXT;
+
+
+import com.android.SdkConstants;
+import static com.android.SdkConstants.ANDROID_URI;
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+
+/**
+ * An {@link IViewRule} for android.widget.SlidingDrawerRule which initializes new sliding
+ * drawers with their mandatory children and default sizing attributes
+ */
+public class SlidingDrawerRule extends BaseLayoutRule {
+
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ if (insertType.isCreate()) {
+ String matchParent = getFillParentValueName();
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, matchParent);
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, matchParent);
+
+ // Create mandatory children and reference them from the handle and content
+ // attributes of the sliding drawer
+ String handleId = "@+id/handle"; //$NON-NLS-1$
+ String contentId = "@+id/content"; //$NON-NLS-1$
+ node.setAttribute(ANDROID_URI, ATTR_HANDLE, handleId);
+ node.setAttribute(ANDROID_URI, ATTR_CONTENT, contentId);
+
+ // Handle
+ INode handle = node.appendChild(SdkConstants.FQCN_BUTTON);
+ handle.setAttribute(ANDROID_URI, ATTR_TEXT, "Handle");
+ handle.setAttribute(ANDROID_URI, ATTR_ID, handleId);
+
+ // Content
+ INode content = node.appendChild(SdkConstants.FQCN_LINEAR_LAYOUT);
+ content.setAttribute(ANDROID_URI, ATTR_ID, contentId);
+ content.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, matchParent);
+ content.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, matchParent);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TabHostRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TabHostRule.java
new file mode 100644
index 000000000..cb2153b50
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TabHostRule.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_ORIENTATION;
+import static com.android.SdkConstants.FQCN_FRAME_LAYOUT;
+import static com.android.SdkConstants.FQCN_LINEAR_LAYOUT;
+import static com.android.SdkConstants.FQCN_TAB_WIDGET;
+import static com.android.SdkConstants.VALUE_VERTICAL;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+
+/**
+ * An {@link IViewRule} for android.widget.TabHost.
+ */
+public class TabHostRule extends IgnoredLayoutRule {
+ // The TabHost layout states in its documentation that you typically
+ // manipulate its children via the TabHost rather than directly manipulating
+ // the child elements yourself, e.g. via addTab() etc.
+
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ if (insertType.isCreate()) {
+ String fillParent = getFillParentValueName();
+
+ // Configure default Table setup as described in the Table tutorial
+ node.setAttribute(ANDROID_URI, ATTR_ID, "@android:id/tabhost"); //$NON-NLS-1$
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent);
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, fillParent);
+
+ INode linear = node.appendChild(FQCN_LINEAR_LAYOUT);
+ linear.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent);
+ linear.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, fillParent);
+ linear.setAttribute(ANDROID_URI, ATTR_ORIENTATION,
+ VALUE_VERTICAL);
+
+ INode tab = linear.appendChild(FQCN_TAB_WIDGET);
+ tab.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent);
+ tab.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
+ tab.setAttribute(ANDROID_URI, ATTR_ID, "@android:id/tabs"); //$NON-NLS-1$
+
+ INode frame = linear.appendChild(FQCN_FRAME_LAYOUT);
+ frame.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent);
+ frame.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, fillParent);
+ frame.setAttribute(ANDROID_URI, ATTR_ID, "@android:id/tabcontent"); //$NON-NLS-1$
+
+ for (int i = 0; i < 3; i++) {
+ INode child = frame.appendChild(FQCN_LINEAR_LAYOUT);
+ child.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent);
+ child.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, fillParent);
+ child.setAttribute(ANDROID_URI, ATTR_ID,
+ String.format("@+id/tab%d", i + 1)); //$NON-NLS-1$
+ }
+ }
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TabWidgetRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TabWidgetRule.java
new file mode 100755
index 000000000..7ebaea54d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TabWidgetRule.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2010 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;
+
+import com.android.ide.common.api.IViewRule;
+
+/**
+ * An {@link IViewRule} for android.widget.TabWidget.
+ */
+public class TabWidgetRule extends IgnoredLayoutRule {
+ // TabWidgets aren't configurable as plain LinearLayout since they
+ // are supposed to be manipulated by their parent TabHost.
+}
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
new file mode 100644
index 000000000..b6aeeb486
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableLayoutRule.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.FQCN_TABLE_ROW;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+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;
+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.RuleAction;
+import com.android.ide.common.api.SegmentType;
+
+import java.net.URL;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An {@link IViewRule} for android.widget.TableLayout.
+ */
+public class TableLayoutRule extends LinearLayoutRule {
+ // A table is a linear layout, but with a few differences:
+ // the default is vertical, not horizontal
+ // The fill of all children should be wrap_content
+
+ private static final String ACTION_ADD_ROW = "_addrow"; //$NON-NLS-1$
+ private static final String ACTION_REMOVE_ROW = "_removerow"; //$NON-NLS-1$
+ private static final URL ICON_ADD_ROW =
+ TableLayoutRule.class.getResource("addrow.png"); //$NON-NLS-1$
+ private static final URL ICON_REMOVE_ROW =
+ TableLayoutRule.class.getResource("removerow.png"); //$NON-NLS-1$
+
+ @Override
+ protected boolean isVertical(INode node) {
+ // Tables are always vertical
+ return true;
+ }
+
+ @Override
+ protected boolean supportsOrientation() {
+ return false;
+ }
+
+ @Override
+ public void onChildInserted(@NonNull INode child, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ // Overridden to inhibit the setting of layout_width/layout_height since
+ // it should always be match_parent
+ }
+
+ /**
+ * Add an explicit "Add Row" action to the context menu
+ */
+ @Override
+ public void addContextMenuActions(@NonNull List<RuleAction> actions,
+ final @NonNull INode selectedNode) {
+ super.addContextMenuActions(actions, selectedNode);
+
+ IMenuCallback addTab = new IMenuCallback() {
+ @Override
+ public void action(
+ @NonNull RuleAction action,
+ @NonNull List<? extends INode> selectedNodes,
+ final @Nullable String valueId,
+ @Nullable Boolean newValue) {
+ final INode node = selectedNode;
+ INode newRow = node.appendChild(FQCN_TABLE_ROW);
+ mRulesEngine.select(Collections.singletonList(newRow));
+ }
+ };
+ actions.add(RuleAction.createAction("_addrow", "Add Row", addTab, null, 5, false)); //$NON-NLS-1$
+ }
+
+ @Override
+ public void addLayoutActions(
+ @NonNull List<RuleAction> actions,
+ final @NonNull INode parentNode,
+ final @NonNull List<? extends INode> children) {
+ super.addLayoutActions(actions, parentNode, children);
+ addTableLayoutActions(mRulesEngine, actions, parentNode, children);
+ }
+
+ /**
+ * Adds layout actions to add and remove toolbar items
+ */
+ static void addTableLayoutActions(final IClientRulesEngine rulesEngine,
+ List<RuleAction> actions, final INode parentNode,
+ final List<? extends INode> children) {
+ IMenuCallback actionCallback = new IMenuCallback() {
+ @Override
+ public void action(
+ final @NonNull RuleAction action,
+ @NonNull List<? extends INode> selectedNodes,
+ final @Nullable String valueId,
+ final @Nullable Boolean newValue) {
+ parentNode.editXml("Add/Remove Table Row", new INodeHandler() {
+ @Override
+ public void handle(@NonNull INode n) {
+ if (action.getId().equals(ACTION_ADD_ROW)) {
+ // Determine the index of the selection, if any; if there is
+ // a selection, insert the row before the current row, otherwise
+ // append it to the table.
+ int index = -1;
+ INode[] rows = parentNode.getChildren();
+ if (children != null) {
+ findTableIndex:
+ for (INode child : children) {
+ // Find direct child of table layout
+ while (child != null && child.getParent() != parentNode) {
+ child = child.getParent();
+ }
+ if (child != null) {
+ // Compute index of direct child of table layout
+ for (int i = 0; i < rows.length; i++) {
+ if (rows[i] == child) {
+ index = i;
+ break findTableIndex;
+ }
+ }
+ }
+ }
+ }
+ INode newRow;
+ if (index == -1) {
+ newRow = parentNode.appendChild(FQCN_TABLE_ROW);
+ } else {
+ newRow = parentNode.insertChildAt(FQCN_TABLE_ROW, index);
+ }
+ rulesEngine.select(Collections.singletonList(newRow));
+ } else if (action.getId().equals(ACTION_REMOVE_ROW)) {
+ // Find the direct children of the TableLayout to delete;
+ // this is necessary since TableRow might also use
+ // this implementation, so the parentNode is the true
+ // TableLayout but the children might be grand children.
+ Set<INode> targets = new HashSet<INode>();
+ for (INode child : children) {
+ while (child != null && child.getParent() != parentNode) {
+ child = child.getParent();
+ }
+ if (child != null) {
+ targets.add(child);
+ }
+ }
+ for (INode target : targets) {
+ parentNode.removeChild(target);
+ }
+ }
+ }
+ });
+ }
+ };
+
+ // Add Row
+ actions.add(RuleAction.createSeparator(150));
+ actions.add(RuleAction.createAction(ACTION_ADD_ROW, "Add Table Row", actionCallback,
+ ICON_ADD_ROW, 160, false));
+
+ // Remove Row (if something is selected)
+ if (children != null && children.size() > 0) {
+ actions.add(RuleAction.createAction(ACTION_REMOVE_ROW, "Remove Table Row",
+ actionCallback, ICON_REMOVE_ROW, 170, false));
+ }
+ }
+
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ if (insertType.isCreate()) {
+ // Start the table with 4 rows
+ for (int i = 0; i < 4; i++) {
+ node.appendChild(FQCN_TABLE_ROW);
+ }
+ }
+ }
+
+ @Override
+ public DropFeedback onResizeBegin(@NonNull INode child, @NonNull INode parent,
+ @Nullable SegmentType horizontalEdge, @Nullable SegmentType verticalEdge,
+ @Nullable Object childView, @Nullable Object parentView) {
+ // 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*/,
+ childView, parentView);
+ }
+}
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
new file mode 100644
index 000000000..6e3f202ee
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableRowRule.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.FQCN_TABLE_LAYOUT;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+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.RuleAction;
+import com.android.ide.common.api.SegmentType;
+
+import java.util.List;
+
+/**
+ * An {@link IViewRule} for android.widget.TableRow.
+ */
+public class TableRowRule extends LinearLayoutRule {
+ @Override
+ protected boolean isVertical(INode node) {
+ return false;
+ }
+
+ @Override
+ protected boolean supportsOrientation() {
+ return false;
+ }
+
+ @Override
+ public void onChildInserted(@NonNull INode child, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ // Overridden to inhibit the setting of layout_width/layout_height since
+ // the table row will enforce match_parent and wrap_content for width and height
+ // respectively.
+ }
+
+ @Override
+ public void addLayoutActions(
+ @NonNull List<RuleAction> actions,
+ final @NonNull INode parentNode,
+ final @NonNull List<? extends INode> children) {
+ super.addLayoutActions(actions, parentNode, children);
+
+ // Also apply table-specific actions on the table row such that you can
+ // select something in a table row and still get offered actions on the surrounding
+ // table.
+ if (children != null) {
+ INode grandParent = parentNode.getParent();
+ if (grandParent != null && grandParent.getFqcn().equals(FQCN_TABLE_LAYOUT)) {
+ TableLayoutRule.addTableLayoutActions(mRulesEngine, actions, grandParent,
+ children);
+ }
+ }
+ }
+
+ @Override
+ public DropFeedback onResizeBegin(@NonNull INode child, @NonNull INode parent,
+ @Nullable SegmentType horizontalEdge, @Nullable SegmentType verticalEdge,
+ @Nullable Object childView, @Nullable Object parentView) {
+ // 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/TimePickerRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TimePickerRule.java
new file mode 100755
index 000000000..1eb603d43
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TimePickerRule.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2010 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;
+
+import com.android.ide.common.api.IViewRule;
+
+/**
+ * An {@link IViewRule} for android.widget.TimePicker.
+ */
+public class TimePickerRule extends IgnoredLayoutRule {
+ // A TimePicker inherits from FrameLayout but is not a general purpose
+ // FrameLayout
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ViewRule.java
new file mode 100755
index 000000000..a7b23ab75
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ViewRule.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2010 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;
+
+import com.android.ide.common.api.IViewRule;
+
+/**
+ * An {@link IViewRule} for android.view.View and all its derived classes. This
+ * is the "root" rule, that is used whenever there is not more specific rule to
+ * apply.
+ * <p/>
+ * There is no customization here, everything that is common to all views is
+ * simply implemented in BaseViewRule.
+ */
+public class ViewRule extends BaseViewRule {
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ViewTagRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ViewTagRule.java
new file mode 100644
index 000000000..a89a3d851
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ViewTagRule.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.common.layout;
+
+import static com.android.SdkConstants.ATTR_CLASS;
+
+import com.android.annotations.NonNull;
+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.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+
+/**
+ * An {@link IViewRule} for the special XML {@code <view>} tag.
+ */
+public class ViewTagRule extends BaseViewRule {
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ // When dropping a view tag, ask the user which custom view class to use
+ if (insertType == InsertType.CREATE) { // NOT InsertType.CREATE_PREVIEW
+ String fqcn = mRulesEngine.displayCustomViewClassInput();
+ if (fqcn != null) {
+ if (!ViewElementDescriptor.viewNeedsPackage(fqcn)) {
+ fqcn = fqcn.substring(fqcn.lastIndexOf('.') + 1);
+ }
+ node.editXml("Set Custom View Class",
+ new PropertySettingNodeHandler(null, ATTR_CLASS,
+ fqcn.length() > 0 ? fqcn : null));
+ } else {
+ // Remove the view; the insertion was canceled
+ parent.removeChild(node);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/WebViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/WebViewRule.java
new file mode 100644
index 000000000..42b06e65b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/WebViewRule.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+
+/**
+ * An {@link IViewRule} for android.widget.ZoomControls.
+ */
+public class WebViewRule extends IgnoredLayoutRule {
+ // A WebView is not a general purpose AbsoluteLayout you should drop stuff
+ // into; it's an AbsoluteLayout for implementation purposes.
+
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ if (insertType.isCreate()) {
+ String matchParent = getFillParentValueName();
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, matchParent);
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, matchParent);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ZoomButtonRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ZoomButtonRule.java
new file mode 100644
index 000000000..66cbd45f0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ZoomButtonRule.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_SRC;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.InsertType;
+
+public class ZoomButtonRule extends BaseViewRule {
+ @Override
+ public void onCreate(@NonNull INode node, @NonNull INode parent,
+ @NonNull InsertType insertType) {
+ super.onCreate(node, parent, insertType);
+
+ if (insertType.isCreate()) {
+ node.setAttribute(ANDROID_URI, ATTR_SRC, "@android:drawable/btn_plus"); //$NON-NLS-1$
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ZoomControlsRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ZoomControlsRule.java
new file mode 100755
index 000000000..226bf4eb5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ZoomControlsRule.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2010 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;
+
+import com.android.ide.common.api.IViewRule;
+
+/**
+ * An {@link IViewRule} for android.widget.ZoomControls.
+ */
+public class ZoomControlsRule extends IgnoredLayoutRule {
+ // A ZoomControl is only a LinearLayout in terms of borrowing
+ // implementation; it does not behave like one in terms of configurability.
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/addcol.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/addcol.png
new file mode 100644
index 000000000..21391ef53
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/addcol.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/addrow.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/addrow.png
new file mode 100644
index 000000000..0faa3e607
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/addrow.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/allweight.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/allweight.png
new file mode 100644
index 000000000..506c66320
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/allweight.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/baseline.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/baseline.png
new file mode 100644
index 000000000..acb187ca0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/baseline.png
Binary files differ
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 000000000..5053cdadd
--- /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 000000000..ebba8e812
--- /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/clearweights.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/clearweights.png
new file mode 100644
index 000000000..ad27c174d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/clearweights.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 000000000..7247d5a09
--- /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/distribute.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/distribute.png
new file mode 100644
index 000000000..eac2340f9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/distribute.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/fillheight.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/fillheight.png
new file mode 100644
index 000000000..38e137deb
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/fillheight.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/fillwidth.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/fillwidth.png
new file mode 100644
index 000000000..f272aab68
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/fillwidth.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/gravity.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/gravity.png
new file mode 100644
index 000000000..4f20928ad
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/gravity.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java
new file mode 100644
index 000000000..8bdb56bfe
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java
@@ -0,0 +1,840 @@
+/*
+ * 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.grid;
+
+import static com.android.SdkConstants.ATTR_COLUMN_COUNT;
+import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
+import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
+import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
+import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
+import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
+import static com.android.ide.common.layout.GravityHelper.getGravity;
+import static com.android.ide.common.layout.GridLayoutRule.GRID_SIZE;
+import static com.android.ide.common.layout.GridLayoutRule.MARGIN_SIZE;
+import static com.android.ide.common.layout.GridLayoutRule.MAX_CELL_DIFFERENCE;
+import static com.android.ide.common.layout.GridLayoutRule.SHORT_GAP_DP;
+import static com.android.ide.common.layout.grid.GridModel.UNDEFINED;
+import static java.lang.Math.abs;
+
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewMetadata;
+import com.android.ide.common.api.Margins;
+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.BaseLayoutRule;
+import com.android.ide.common.layout.GravityHelper;
+import com.android.ide.common.layout.GridLayoutRule;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * The {@link GridDropHandler} handles drag and drop operations into and within a
+ * GridLayout, computing guidelines, handling drops to edit the grid model, and so on.
+ */
+public class GridDropHandler {
+ private final GridModel mGrid;
+ private final GridLayoutRule mRule;
+ private GridMatch mColumnMatch;
+ private GridMatch mRowMatch;
+
+ /**
+ * Creates a new {@link GridDropHandler} for
+ * @param gridLayoutRule the corresponding {@link GridLayoutRule}
+ * @param layout the GridLayout node
+ * @param view the view instance of the grid layout receiving the drop
+ */
+ public GridDropHandler(GridLayoutRule gridLayoutRule, INode layout, Object view) {
+ mRule = gridLayoutRule;
+ mGrid = GridModel.get(mRule.getRulesEngine(), layout, view);
+ }
+
+ /**
+ * Computes the best horizontal and vertical matches for a drag to the given position.
+ *
+ * @param feedback a {@link DropFeedback} object containing drag state like the drag
+ * bounds and the drag baseline
+ * @param p the mouse position
+ */
+ public void computeMatches(DropFeedback feedback, Point p) {
+ mRowMatch = mColumnMatch = null;
+ feedback.tooltip = null;
+
+ Rect bounds = mGrid.layout.getBounds();
+ int x1 = p.x;
+ int y1 = p.y;
+
+ Rect dragBounds = feedback.dragBounds;
+ int w = dragBounds != null ? dragBounds.w : 0;
+ int h = dragBounds != null ? dragBounds.h : 0;
+ if (!GridLayoutRule.sGridMode) {
+ if (dragBounds != null) {
+ // Sometimes the items are centered under the mouse so
+ // offset by the top left corner distance
+ x1 += dragBounds.x;
+ y1 += dragBounds.y;
+ }
+
+ int x2 = x1 + w;
+ int y2 = y1 + h;
+
+ if (x2 < bounds.x || y2 < bounds.y || x1 > bounds.x2() || y1 > bounds.y2()) {
+ return;
+ }
+
+ List<GridMatch> columnMatches = new ArrayList<GridMatch>();
+ List<GridMatch> rowMatches = new ArrayList<GridMatch>();
+ int max = BaseLayoutRule.getMaxMatchDistance();
+
+ // Column matches:
+ addLeftSideMatch(x1, columnMatches, max);
+ addRightSideMatch(x2, columnMatches, max);
+ addCenterColumnMatch(bounds, x1, y1, x2, y2, columnMatches, max);
+
+ // Row matches:
+ int row = (mGrid.getViewCount() == 0) ? 0 : mGrid.getClosestRow(y1);
+ int rowY = mGrid.getRowY(row);
+ addTopMatch(y1, rowMatches, max, row, rowY);
+ addBaselineMatch(feedback.dragBaseline, y1, rowMatches, max, row, rowY);
+ addBottomMatch(y2, rowMatches, max);
+
+ // Look for gap-matches: Predefined spacing between widgets.
+ // TODO: Make this use metadata for predefined spacing between
+ // pairs of types of components. For example, buttons have certain
+ // inserts in their 9-patch files (depending on the theme) that should
+ // be considered and subtracted from the overall proposed distance!
+ addColumnGapMatch(bounds, x1, x2, columnMatches, max);
+ addRowGapMatch(bounds, y1, y2, rowMatches, max);
+
+ // Fallback: Split existing cell. Also do snap-to-grid.
+ if (GridLayoutRule.sSnapToGrid) {
+ x1 = ((x1 - MARGIN_SIZE - bounds.x) / GRID_SIZE) * GRID_SIZE
+ + MARGIN_SIZE + bounds.x;
+ y1 = ((y1 - MARGIN_SIZE - bounds.y) / GRID_SIZE) * GRID_SIZE
+ + MARGIN_SIZE + bounds.y;
+ x2 = x1 + w;
+ y2 = y1 + h;
+ }
+
+
+ if (columnMatches.size() == 0 && x1 >= bounds.x) {
+ // Split the current cell since we have no matches
+ // TODO: Decide whether it should be gravity left or right...
+ columnMatches.add(new GridMatch(SegmentType.LEFT, 0, x1, mGrid.getColumn(x1),
+ true /* createCell */, UNDEFINED));
+ }
+ if (rowMatches.size() == 0 && y1 >= bounds.y) {
+ rowMatches.add(new GridMatch(SegmentType.TOP, 0, y1, mGrid.getRow(y1),
+ true /* createCell */, UNDEFINED));
+ }
+
+ // Pick best matches
+ Collections.sort(rowMatches);
+ Collections.sort(columnMatches);
+
+ mColumnMatch = null;
+ mRowMatch = null;
+ String columnDescription = null;
+ String rowDescription = null;
+ if (columnMatches.size() > 0) {
+ mColumnMatch = columnMatches.get(0);
+ columnDescription = mColumnMatch.getDisplayName(mGrid.layout);
+ }
+ if (rowMatches.size() > 0) {
+ mRowMatch = rowMatches.get(0);
+ rowDescription = mRowMatch.getDisplayName(mGrid.layout);
+ }
+
+ if (columnDescription != null && rowDescription != null) {
+ feedback.tooltip = columnDescription + '\n' + rowDescription;
+ }
+
+ feedback.invalidTarget = mColumnMatch == null || mRowMatch == null;
+ } else {
+ // Find which cell we're inside.
+
+ // TODO: Find out where within the cell we are, and offer to tweak the gravity
+ // based on the position.
+ int column = mGrid.getColumn(x1);
+ int row = mGrid.getRow(y1);
+
+ int leftDistance = mGrid.getColumnDistance(column, x1);
+ int rightDistance = mGrid.getColumnDistance(column + 1, x1);
+ int topDistance = mGrid.getRowDistance(row, y1);
+ int bottomDistance = mGrid.getRowDistance(row + 1, y1);
+
+ int SLOP = 2;
+ int radius = mRule.getNewCellSize();
+ if (rightDistance < radius + SLOP) {
+ column = Math.min(column + 1, mGrid.actualColumnCount);
+ leftDistance = rightDistance;
+ }
+ if (bottomDistance < radius + SLOP) {
+ row = Math.min(row + 1, mGrid.actualRowCount);
+ topDistance = bottomDistance;
+ }
+
+ boolean createColumn = leftDistance < radius + SLOP;
+ boolean createRow = topDistance < radius + SLOP;
+ if (x1 >= bounds.x2()) {
+ createColumn = true;
+ }
+ if (y1 >= bounds.y2()) {
+ createRow = true;
+ }
+
+ int cellWidth = leftDistance + rightDistance;
+ int cellHeight = topDistance + bottomDistance;
+ SegmentType horizontalType = SegmentType.LEFT;
+ SegmentType verticalType = SegmentType.TOP;
+ int minDistance = 10; // Don't center or right/bottom align in tiny cells
+ if (!createColumn && leftDistance > minDistance
+ && dragBounds != null && dragBounds.w < cellWidth - 10) {
+ if (rightDistance < leftDistance) {
+ horizontalType = SegmentType.RIGHT;
+ }
+
+ int centerDistance = Math.abs(cellWidth / 2 - leftDistance);
+ if (centerDistance < leftDistance / 2 && centerDistance < rightDistance / 2) {
+ horizontalType = SegmentType.CENTER_HORIZONTAL;
+ }
+ }
+ if (!createRow && topDistance > minDistance
+ && dragBounds != null && dragBounds.h < cellHeight - 10) {
+ if (bottomDistance < topDistance) {
+ verticalType = SegmentType.BOTTOM;
+ }
+ int centerDistance = Math.abs(cellHeight / 2 - topDistance);
+ if (centerDistance < topDistance / 2 && centerDistance < bottomDistance / 2) {
+ verticalType = SegmentType.CENTER_VERTICAL;
+ }
+ }
+
+ mColumnMatch = new GridMatch(horizontalType, 0, x1, column, createColumn, 0);
+ mRowMatch = new GridMatch(verticalType, 0, y1, row, createRow, 0);
+
+ StringBuilder description = new StringBuilder(50);
+ String rowString = Integer.toString(mColumnMatch.cellIndex + 1);
+ String columnString = Integer.toString(mRowMatch.cellIndex + 1);
+ if (mRowMatch.createCell && mRowMatch.cellIndex < mGrid.actualRowCount) {
+ description.append(String.format("Shift row %1$d down", mRowMatch.cellIndex + 1));
+ description.append('\n');
+ }
+ if (mColumnMatch.createCell && mColumnMatch.cellIndex < mGrid.actualColumnCount) {
+ description.append(String.format("Shift column %1$d right",
+ mColumnMatch.cellIndex + 1));
+ description.append('\n');
+ }
+ description.append(String.format("Insert into cell (%1$s,%2$s)",
+ rowString, columnString));
+ description.append('\n');
+ description.append(String.format("Align %1$s, %2$s",
+ horizontalType.name().toLowerCase(Locale.US),
+ verticalType.name().toLowerCase(Locale.US)));
+ feedback.tooltip = description.toString();
+ }
+ }
+
+ /**
+ * Adds a match to align the left edge with some other edge.
+ */
+ private void addLeftSideMatch(int x1, List<GridMatch> columnMatches, int max) {
+ int column = (mGrid.getViewCount() == 0) ? 0 : mGrid.getClosestColumn(x1);
+ int columnX = mGrid.getColumnX(column);
+ int distance = abs(columnX - x1);
+ if (distance <= max) {
+ columnMatches.add(new GridMatch(SegmentType.LEFT, distance, columnX, column,
+ false, UNDEFINED));
+ }
+ }
+
+ /**
+ * Adds a match to align the right edge with some other edge.
+ */
+ private void addRightSideMatch(int x2, List<GridMatch> columnMatches, int max) {
+ // TODO: Only match the right hand side if the drag bounds fit fully within the
+ // cell! Ditto for match below.
+ int columnRight = (mGrid.getViewCount() == 0) ? 0 : mGrid.getClosestColumn(x2);
+ int rightDistance = mGrid.getColumnDistance(columnRight, x2);
+ if (rightDistance < max) {
+ int columnX = mGrid.getColumnX(columnRight);
+ if (columnX > mGrid.layout.getBounds().x) {
+ columnMatches.add(new GridMatch(SegmentType.RIGHT, rightDistance, columnX,
+ columnRight, false, UNDEFINED));
+ }
+ }
+ }
+
+ /**
+ * Adds a horizontal match with the center axis of the GridLayout
+ */
+ private void addCenterColumnMatch(Rect bounds, int x1, int y1, int x2, int y2,
+ List<GridMatch> columnMatches, int max) {
+ Collection<INode> intersectsRow = mGrid.getIntersectsRow(y1, y2);
+ if (intersectsRow.size() == 0) {
+ // Offer centering on this row since there isn't anything there
+ int matchedLine = bounds.centerX();
+ int distance = abs((x1 + x2) / 2 - matchedLine);
+ if (distance <= 2 * max) {
+ boolean createCell = false; // always just put in column 0
+ columnMatches.add(new GridMatch(SegmentType.CENTER_HORIZONTAL, distance,
+ matchedLine, 0 /* column */, createCell, UNDEFINED));
+ }
+ }
+ }
+
+ /**
+ * Adds a match to align the top edge with some other edge.
+ */
+ private void addTopMatch(int y1, List<GridMatch> rowMatches, int max, int row, int rowY) {
+ int distance = mGrid.getRowDistance(row, y1);
+ if (distance <= max) {
+ rowMatches.add(new GridMatch(SegmentType.TOP, distance, rowY, row, false,
+ UNDEFINED));
+ }
+ }
+
+ /**
+ * Adds a match to align the bottom edge with some other edge.
+ */
+ private void addBottomMatch(int y2, List<GridMatch> rowMatches, int max) {
+ int rowBottom = (mGrid.getViewCount() == 0) ? 0 : mGrid.getClosestRow(y2);
+ int distance = mGrid.getRowDistance(rowBottom, y2);
+ if (distance < max) {
+ int rowY = mGrid.getRowY(rowBottom);
+ if (rowY > mGrid.layout.getBounds().y) {
+ rowMatches.add(new GridMatch(SegmentType.BOTTOM, distance, rowY,
+ rowBottom, false, UNDEFINED));
+ }
+ }
+ }
+
+ /**
+ * Adds a baseline match, if applicable.
+ */
+ private void addBaselineMatch(int dragBaseline, int y1, List<GridMatch> rowMatches, int max,
+ int row, int rowY) {
+ int dragBaselineY = y1 + dragBaseline;
+ int rowBaseline = mGrid.getBaseline(row);
+ if (rowBaseline != -1) {
+ int rowBaselineY = rowY + rowBaseline;
+ int distance = abs(dragBaselineY - rowBaselineY);
+ if (distance < max) {
+ rowMatches.add(new GridMatch(SegmentType.BASELINE, distance, rowBaselineY, row,
+ false, UNDEFINED));
+ }
+ }
+ }
+
+ /**
+ * Computes a horizontal "gap" match - a preferred distance from the nearest edge,
+ * including margin edges
+ */
+ private void addColumnGapMatch(Rect bounds, int x1, int x2, List<GridMatch> columnMatches,
+ int max) {
+ if (x1 < bounds.x + MARGIN_SIZE + max) {
+ int matchedLine = bounds.x + MARGIN_SIZE;
+ int distance = abs(matchedLine - x1);
+ if (distance <= max) {
+ boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine;
+ columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine,
+ 0, createCell, MARGIN_SIZE));
+ }
+ } else if (x2 > bounds.x2() - MARGIN_SIZE - max) {
+ int matchedLine = bounds.x2() - MARGIN_SIZE;
+ int distance = abs(matchedLine - x2);
+ if (distance <= max) {
+ // This does not yet work properly; we need to use columnWeights to achieve this
+ //boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine;
+ //columnMatches.add(new GridMatch(SegmentType.RIGHT, distance, matchedLine,
+ // mGrid.actualColumnCount - 1, createCell, MARGIN_SIZE));
+ }
+ } else {
+ int columnRight = mGrid.getColumn(x1 - SHORT_GAP_DP);
+ int columnX = mGrid.getColumnMaxX(columnRight);
+ int matchedLine = columnX + SHORT_GAP_DP;
+ int distance = abs(matchedLine - x1);
+ if (distance <= max) {
+ boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine;
+ columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine,
+ columnRight, createCell, SHORT_GAP_DP));
+ }
+
+ // Add a column directly adjacent (no gap)
+ columnRight = mGrid.getColumn(x1);
+ columnX = mGrid.getColumnMaxX(columnRight);
+ matchedLine = columnX;
+ distance = abs(matchedLine - x1);
+
+ // Let's say you have this arrangement:
+ // [button1][button2]
+ // This is two columns, where the right hand side edge of column 1 is
+ // flush with the left side edge of column 2, because in fact the width of
+ // button1 is what defines the width of column 1, and that in turn is what
+ // defines the left side position of column 2.
+ //
+ // In this case we don't want to consider inserting a new column at the
+ // right hand side of button1 a better match than matching left on column 2.
+ // Therefore, to ensure that this doesn't happen, we "penalize" right column
+ // matches such that they don't get preferential treatment when the matching
+ // line is on the left side of the column.
+ distance += 2;
+
+ if (distance <= max) {
+ boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine;
+ columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine,
+ columnRight, createCell, 0));
+ }
+ }
+ }
+
+ /**
+ * Computes a vertical "gap" match - a preferred distance from the nearest edge,
+ * including margin edges
+ */
+ private void addRowGapMatch(Rect bounds, int y1, int y2, List<GridMatch> rowMatches, int max) {
+ if (y1 < bounds.y + MARGIN_SIZE + max) {
+ int matchedLine = bounds.y + MARGIN_SIZE;
+ int distance = abs(matchedLine - y1);
+ if (distance <= max) {
+ boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine;
+ rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine,
+ 0, createCell, MARGIN_SIZE));
+ }
+ } else if (y2 > bounds.y2() - MARGIN_SIZE - max) {
+ int matchedLine = bounds.y2() - MARGIN_SIZE;
+ int distance = abs(matchedLine - y2);
+ if (distance <= max) {
+ // This does not yet work properly; we need to use columnWeights to achieve this
+ //boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine;
+ //rowMatches.add(new GridMatch(SegmentType.BOTTOM, distance, matchedLine,
+ // mGrid.actualRowCount - 1, createCell, MARGIN_SIZE));
+ }
+ } else {
+ int rowBottom = mGrid.getRow(y1 - SHORT_GAP_DP);
+ int rowY = mGrid.getRowMaxY(rowBottom);
+ int matchedLine = rowY + SHORT_GAP_DP;
+ int distance = abs(matchedLine - y1);
+ if (distance <= max) {
+ boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine;
+ rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine,
+ rowBottom, createCell, SHORT_GAP_DP));
+ }
+
+ // Add a row directly adjacent (no gap)
+ rowBottom = mGrid.getRow(y1);
+ rowY = mGrid.getRowMaxY(rowBottom);
+ matchedLine = rowY;
+ distance = abs(matchedLine - y1);
+ distance += 2; // See explanation in addColumnGapMatch
+ if (distance <= max) {
+ boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine;
+ rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine,
+ rowBottom, createCell, 0));
+ }
+
+ }
+ }
+
+ /**
+ * Called when a node is dropped in free-form mode. This will insert the dragged
+ * element into the grid and returns the newly created node.
+ *
+ * @param targetNode the GridLayout node
+ * @param element the dragged element
+ * @return the newly created {@link INode}
+ */
+ public INode handleFreeFormDrop(INode targetNode, IDragElement element) {
+ assert mRowMatch != null;
+ assert mColumnMatch != null;
+
+ String fqcn = element.getFqcn();
+
+ INode newChild = null;
+
+ Rect bounds = element.getBounds();
+ int row = mRowMatch.cellIndex;
+ int column = mColumnMatch.cellIndex;
+
+ if (targetNode.getChildren().length == 0) {
+ //
+ // Set up the initial structure:
+ //
+ //
+ // Fixed Fixed
+ // Size Size
+ // Column Expanding Column Column
+ // +-----+-------------------------------+-----+
+ // | | | |
+ // | 0,0 | 0,1 | 0,2 | Fixed Size Row
+ // | | | |
+ // +-----+-------------------------------+-----+
+ // | | | |
+ // | | | |
+ // | | | |
+ // | 1,0 | 1,1 | 1,2 | Expanding Row
+ // | | | |
+ // | | | |
+ // | | | |
+ // +-----+-------------------------------+-----+
+ // | | | |
+ // | 2,0 | 2,1 | 2,2 | Fixed Size Row
+ // | | | |
+ // +-----+-------------------------------+-----+
+ //
+ // This is implemented in GridLayout by the following grid, where
+ // SC1 has columnWeight=1 and SR1 has rowWeight=1.
+ // (SC=Space for Column, SR=Space for Row)
+ //
+ // +------+-------------------------------+------+
+ // | | | |
+ // | SCR0 | SC1 | SC2 |
+ // | | | |
+ // +------+-------------------------------+------+
+ // | | | |
+ // | | | |
+ // | | | |
+ // | SR1 | | |
+ // | | | |
+ // | | | |
+ // | | | |
+ // +------+-------------------------------+------+
+ // | | | |
+ // | SR2 | | |
+ // | | | |
+ // +------+-------------------------------+------+
+ //
+ // Note that when we split columns and rows here, if splitting the expanding
+ // row or column then the row or column weight should be moved to the right or
+ // bottom half!
+
+
+ //int columnX = mGrid.getColumnX(column);
+ //int rowY = mGrid.getRowY(row);
+
+ mGrid.setGridAttribute(targetNode, ATTR_COLUMN_COUNT, 2);
+ //mGrid.setGridAttribute(targetNode, ATTR_COLUMN_COUNT, 3);
+ //INode scr0 = addSpacer(targetNode, -1, 0, 0, 1, 1);
+ //INode sc1 = addSpacer(targetNode, -1, 0, 1, 0, 0);
+ //INode sc2 = addSpacer(targetNode, -1, 0, 2, 1, 0);
+ //INode sr1 = addSpacer(targetNode, -1, 1, 0, 0, 0);
+ //INode sr2 = addSpacer(targetNode, -1, 2, 0, 0, 1);
+ //mGrid.setGridAttribute(sc1, ATTR_LAYOUT_GRAVITY, VALUE_FILL_HORIZONTAL);
+ //mGrid.setGridAttribute(sr1, ATTR_LAYOUT_GRAVITY, VALUE_FILL_VERTICAL);
+ //
+ //mGrid.loadFromXml();
+ //column = mGrid.getColumn(columnX);
+ //row = mGrid.getRow(rowY);
+ }
+
+ int startX, endX;
+ if (mColumnMatch.type == SegmentType.RIGHT) {
+ endX = mColumnMatch.matchedLine - 1;
+ startX = endX - bounds.w;
+ column = mGrid.getColumn(startX);
+ } else {
+ startX = mColumnMatch.matchedLine; // TODO: What happens on type=RIGHT?
+ endX = startX + bounds.w;
+ }
+ int startY, endY;
+ if (mRowMatch.type == SegmentType.BOTTOM) {
+ endY = mRowMatch.matchedLine - 1;
+ startY = endY - bounds.h;
+ row = mGrid.getRow(startY);
+ } else if (mRowMatch.type == SegmentType.BASELINE) {
+ // TODO: The rowSpan should always be 1 for baseline alignments, since
+ // otherwise the alignment won't work!
+ startY = endY = mRowMatch.matchedLine;
+ } else {
+ startY = mRowMatch.matchedLine;
+ endY = startY + bounds.h;
+ }
+ int endColumn = mGrid.getColumn(endX);
+ int endRow = mGrid.getRow(endY);
+ int columnSpan = endColumn - column + 1;
+ int rowSpan = endRow - row + 1;
+
+ // Make sure my math was right:
+ assert mRowMatch.type != SegmentType.BASELINE || rowSpan == 1 : rowSpan;
+
+ // If the item almost fits into the row (at most N % bigger) then just enlarge
+ // the row; don't add a rowspan since that will defeat baseline alignment etc
+ if (!mRowMatch.createCell && bounds.h <= MAX_CELL_DIFFERENCE * mGrid.getRowHeight(
+ mRowMatch.type == SegmentType.BOTTOM ? endRow : row, 1)) {
+ if (mRowMatch.type == SegmentType.BOTTOM) {
+ row += rowSpan - 1;
+ }
+ rowSpan = 1;
+ }
+ if (!mColumnMatch.createCell && bounds.w <= MAX_CELL_DIFFERENCE * mGrid.getColumnWidth(
+ mColumnMatch.type == SegmentType.RIGHT ? endColumn : column, 1)) {
+ if (mColumnMatch.type == SegmentType.RIGHT) {
+ column += columnSpan - 1;
+ }
+ columnSpan = 1;
+ }
+
+ if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) {
+ column = 0;
+ columnSpan = mGrid.actualColumnCount;
+ }
+
+ // Temporary: Ensure we don't get in trouble with implicit positions
+ mGrid.applyPositionAttributes();
+
+ // Split cells to make a new column
+ if (mColumnMatch.createCell) {
+ int columnWidthPx = mGrid.getColumnDistance(column, mColumnMatch.matchedLine);
+ //assert columnWidthPx == columnMatch.distance; // TBD? IF so simplify
+ int columnWidthDp = mRule.getRulesEngine().pxToDp(columnWidthPx);
+
+ int maxX = mGrid.getColumnMaxX(column);
+ boolean insertMarginColumn = false;
+ if (mColumnMatch.margin == 0) {
+ columnWidthDp = 0;
+ } else if (mColumnMatch.margin != UNDEFINED) {
+ int distance = abs(mColumnMatch.matchedLine - (maxX + mColumnMatch.margin));
+ insertMarginColumn = column > 0 && distance < 2;
+ if (insertMarginColumn) {
+ int margin = mColumnMatch.margin;
+ if (ViewMetadataRepository.INSETS_SUPPORTED) {
+ IViewMetadata metadata = mRule.getRulesEngine().getMetadata(fqcn);
+ if (metadata != null) {
+ Margins insets = metadata.getInsets();
+ if (insets != null) {
+ // TODO:
+ // Consider left or right side attachment
+ // TODO: Also consider inset of element on cell to the left
+ margin -= insets.left;
+ }
+ }
+ }
+
+ columnWidthDp = mRule.getRulesEngine().pxToDp(margin);
+ }
+ }
+
+ column++;
+ mGrid.splitColumn(column, insertMarginColumn, columnWidthDp, mColumnMatch.matchedLine);
+ if (insertMarginColumn) {
+ column++;
+ }
+ }
+
+ // Split cells to make a new row
+ if (mRowMatch.createCell) {
+ int rowHeightPx = mGrid.getRowDistance(row, mRowMatch.matchedLine);
+ //assert rowHeightPx == rowMatch.distance; // TBD? If so simplify
+ int rowHeightDp = mRule.getRulesEngine().pxToDp(rowHeightPx);
+
+ int maxY = mGrid.getRowMaxY(row);
+ boolean insertMarginRow = false;
+ if (mRowMatch.margin == 0) {
+ rowHeightDp = 0;
+ } else if (mRowMatch.margin != UNDEFINED) {
+ int distance = abs(mRowMatch.matchedLine - (maxY + mRowMatch.margin));
+ insertMarginRow = row > 0 && distance < 2;
+ if (insertMarginRow) {
+ int margin = mRowMatch.margin;
+ IViewMetadata metadata = mRule.getRulesEngine().getMetadata(element.getFqcn());
+ if (metadata != null) {
+ Margins insets = metadata.getInsets();
+ if (insets != null) {
+ // TODO:
+ // Consider left or right side attachment
+ // TODO: Also consider inset of element on cell to the left
+ margin -= insets.top;
+ }
+ }
+
+ rowHeightDp = mRule.getRulesEngine().pxToDp(margin);
+ }
+ }
+
+ row++;
+ mGrid.splitRow(row, insertMarginRow, rowHeightDp, mRowMatch.matchedLine);
+ if (insertMarginRow) {
+ row++;
+ }
+ }
+
+ // Figure out where to insert the new child
+
+ int index = mGrid.getInsertIndex(row, column);
+ if (index == -1) {
+ // Couldn't find a later place to insert
+ newChild = targetNode.appendChild(fqcn);
+ } else {
+ GridModel.ViewData next = mGrid.getView(index);
+
+ newChild = targetNode.insertChildAt(fqcn, index);
+
+ // Must also apply positions to the following child to ensure
+ // that the new child doesn't affect the implicit numbering!
+ // TODO: We can later check whether the implied number is equal to
+ // what it already is such that we don't need this
+ next.applyPositionAttributes();
+ }
+
+ // Set the cell position (gravity) of the new widget
+ int gravity = 0;
+ if (mColumnMatch.type == SegmentType.RIGHT) {
+ gravity |= GravityHelper.GRAVITY_RIGHT;
+ } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) {
+ gravity |= GravityHelper.GRAVITY_CENTER_HORIZ;
+ }
+ mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, column);
+ if (mRowMatch.type == SegmentType.BASELINE) {
+ // There *is* no baseline gravity constant, instead, leave the
+ // vertical gravity unspecified and GridLayout will treat it as
+ // baseline alignment
+ //gravity |= GravityHelper.GRAVITY_BASELINE;
+ } else if (mRowMatch.type == SegmentType.BOTTOM) {
+ gravity |= GravityHelper.GRAVITY_BOTTOM;
+ } else if (mRowMatch.type == SegmentType.CENTER_VERTICAL) {
+ gravity |= GravityHelper.GRAVITY_CENTER_VERT;
+ }
+ // Ensure that we have at least one horizontal and vertical constraint, otherwise
+ // the new item will be fixed. As an example, if we have a single button in the
+ // table which we inserted *without* a gravity, and we then insert a button
+ // above it with a vertical gravity, then only the top column would be considered
+ // stretchable, and it will fill all available vertical space and the previous
+ // button will jump to the bottom.
+ if (!GravityHelper.isConstrainedHorizontally(gravity)) {
+ gravity |= GravityHelper.GRAVITY_LEFT;
+ }
+ /* This causes problems: Try placing two buttons vertically from the top of the layout.
+ We need to solve the free column/free row problem first.
+ if (!GravityHelper.isConstrainedVertically(gravity)
+ // There is no baseline constant, so we have to leave it unconstrained instead
+ && mRowMatch.type != SegmentType.BASELINE
+ // You also can't baseline align one element with another that has vertical
+ // alignment top or bottom, so when we first "freely" place views (e.g.
+ // at a particular y location), also place it freely (no constraint).
+ && !mRowMatch.createCell) {
+ gravity |= GravityHelper.GRAVITY_TOP;
+ }
+ */
+ mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, getGravity(gravity));
+
+ mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, row);
+
+ // Apply spans to ensure that the widget can fit without pushing columns
+ if (columnSpan > 1) {
+ mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN_SPAN, columnSpan);
+ }
+ if (rowSpan > 1) {
+ mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW_SPAN, rowSpan);
+ }
+
+ // Ensure that we don't store columnCount=0
+ if (mGrid.actualColumnCount == 0) {
+ mGrid.setGridAttribute(mGrid.layout, ATTR_COLUMN_COUNT, Math.max(1, column + 1));
+ }
+
+ return newChild;
+ }
+
+ /**
+ * Called when a drop is completed and we're in grid-editing mode. This will insert
+ * the dragged element into the target cell.
+ *
+ * @param targetNode the GridLayout node
+ * @param element the dragged element
+ * @return the newly created node
+ */
+ public INode handleGridModeDrop(INode targetNode, IDragElement element) {
+ String fqcn = element.getFqcn();
+ INode newChild = targetNode.appendChild(fqcn);
+
+ int column = mColumnMatch.cellIndex;
+ if (mColumnMatch.createCell) {
+ mGrid.addColumn(column,
+ newChild, UNDEFINED, false, UNDEFINED, UNDEFINED);
+ }
+ int row = mRowMatch.cellIndex;
+ if (mRowMatch.createCell) {
+ mGrid.addRow(row, newChild, UNDEFINED, false, UNDEFINED, UNDEFINED);
+ }
+
+ mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, column);
+ mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, row);
+
+ int gravity = 0;
+ if (mColumnMatch.type == SegmentType.RIGHT) {
+ gravity |= GravityHelper.GRAVITY_RIGHT;
+ } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) {
+ gravity |= GravityHelper.GRAVITY_CENTER_HORIZ;
+ }
+ if (mRowMatch.type == SegmentType.BASELINE) {
+ // There *is* no baseline gravity constant, instead, leave the
+ // vertical gravity unspecified and GridLayout will treat it as
+ // baseline alignment
+ //gravity |= GravityHelper.GRAVITY_BASELINE;
+ } else if (mRowMatch.type == SegmentType.BOTTOM) {
+ gravity |= GravityHelper.GRAVITY_BOTTOM;
+ } else if (mRowMatch.type == SegmentType.CENTER_VERTICAL) {
+ gravity |= GravityHelper.GRAVITY_CENTER_VERT;
+ }
+ if (!GravityHelper.isConstrainedHorizontally(gravity)) {
+ gravity |= GravityHelper.GRAVITY_LEFT;
+ }
+ if (!GravityHelper.isConstrainedVertically(gravity)) {
+ gravity |= GravityHelper.GRAVITY_TOP;
+ }
+ mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, getGravity(gravity));
+
+ if (mGrid.declaredColumnCount == UNDEFINED || mGrid.declaredColumnCount < column + 1) {
+ mGrid.setGridAttribute(mGrid.layout, ATTR_COLUMN_COUNT, column + 1);
+ }
+
+ return newChild;
+ }
+
+ /**
+ * Returns the best horizontal match
+ *
+ * @return the best horizontal match, or null if there is no match
+ */
+ public GridMatch getColumnMatch() {
+ return mColumnMatch;
+ }
+
+ /**
+ * Returns the best vertical match
+ *
+ * @return the best vertical match, or null if there is no match
+ */
+ public GridMatch getRowMatch() {
+ return mRowMatch;
+ }
+
+ /**
+ * Returns the grid used by the drop handler
+ *
+ * @return the grid used by the drop handler, never null
+ */
+ public GridModel getGrid() {
+ return mGrid;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridLayoutPainter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridLayoutPainter.java
new file mode 100644
index 000000000..7e2d3a799
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridLayoutPainter.java
@@ -0,0 +1,370 @@
+/*
+ * 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.grid;
+
+import static com.android.ide.common.layout.GridLayoutRule.GRID_SIZE;
+import static com.android.ide.common.layout.GridLayoutRule.MARGIN_SIZE;
+import static com.android.ide.common.layout.grid.GridModel.UNDEFINED;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IDragElement;
+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.Rect;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.common.layout.GridLayoutRule;
+import com.android.utils.Pair;
+
+/**
+ * Painter which paints feedback during drag, drop and resizing operations, as well as
+ * static selection feedback
+ */
+public class GridLayoutPainter {
+
+ /**
+ * Creates a painter for drop feedback
+ *
+ * @param rule the corresponding {@link GridLayoutRule}
+ * @param elements the dragged elements
+ * @return a {@link IFeedbackPainter} which can paint the drop feedback
+ */
+ public static IFeedbackPainter createDropFeedbackPainter(GridLayoutRule rule,
+ IDragElement[] elements) {
+ return new DropFeedbackPainter(rule, elements);
+ }
+
+ /**
+ * Paints the structure (the grid model) of the given GridLayout.
+ *
+ * @param style the drawing style to use to paint the structure lines
+ * @param layout the grid layout node
+ * @param gc the graphics context to paint into
+ * @param grid the grid model to be visualized
+ */
+ public static void paintStructure(DrawingStyle style, INode layout, IGraphics gc,
+ GridModel grid) {
+ Rect b = layout.getBounds();
+
+ gc.useStyle(style);
+ for (int row = 0; row < grid.actualRowCount; row++) {
+ int y = grid.getRowY(row);
+ gc.drawLine(b.x, y, b.x2(), y);
+ }
+ for (int column = 0; column < grid.actualColumnCount; column++) {
+ int x = grid.getColumnX(column);
+ gc.drawLine(x, b.y, x, b.y2());
+ }
+ }
+
+ /**
+ * Paints a regular grid according to the {@link GridLayoutRule#GRID_SIZE} and
+ * {@link GridLayoutRule#MARGIN_SIZE} dimensions. These are the same lines that
+ * snap-to-grid will align with.
+ *
+ * @param layout the GridLayout node
+ * @param gc the graphics context to paint the grid into
+ */
+ public static void paintGrid(INode layout, IGraphics gc) {
+ Rect b = layout.getBounds();
+
+ int oldAlpha = gc.getAlpha();
+ gc.useStyle(DrawingStyle.GUIDELINE);
+ gc.setAlpha(128);
+
+ int y1 = b.y + MARGIN_SIZE;
+ int y2 = b.y2() - MARGIN_SIZE;
+ for (int y = y1; y < y2; y += GRID_SIZE) {
+ int x1 = b.x + MARGIN_SIZE;
+ int x2 = b.x2() - MARGIN_SIZE;
+ for (int x = x1; x < x2; x += GRID_SIZE) {
+ gc.drawPoint(x, y);
+ }
+ }
+ gc.setAlpha(oldAlpha);
+ }
+
+ /**
+ * Paint resizing feedback (which currently paints the grid model faintly.)
+ *
+ * @param gc the graphics context
+ * @param layout the GridLayout
+ * @param grid the grid model
+ */
+ public static void paintResizeFeedback(IGraphics gc, INode layout, GridModel grid) {
+ paintStructure(DrawingStyle.GRID, layout, gc, grid);
+ }
+
+ /**
+ * A painter which can paint the drop feedback for elements being dragged into or
+ * within a GridLayout.
+ */
+ private static class DropFeedbackPainter implements IFeedbackPainter {
+ private final GridLayoutRule mRule;
+ private final IDragElement[] mElements;
+
+ /** Constructs a new {@link GridLayoutPainter} bound to the given {@link GridLayoutRule}
+ * @param rule the corresponding rule
+ * @param elements the elements to draw */
+ public DropFeedbackPainter(GridLayoutRule rule, IDragElement[] elements) {
+ mRule = rule;
+ mElements = elements;
+ }
+
+ // Implements IFeedbackPainter
+ @Override
+ public void paint(@NonNull IGraphics gc, @NonNull INode node,
+ @NonNull DropFeedback feedback) {
+ Rect b = node.getBounds();
+ if (!b.isValid()) {
+ return;
+ }
+
+ // Highlight the receiver
+ gc.useStyle(DrawingStyle.DROP_RECIPIENT);
+ gc.drawRect(b);
+ GridDropHandler data = (GridDropHandler) feedback.userData;
+
+ if (!GridLayoutRule.sGridMode) {
+ paintFreeFormDropFeedback(gc, node, feedback, b, data);
+ } else {
+ paintGridModeDropFeedback(gc, b, data);
+ }
+ }
+
+ /**
+ * Paints the drag feedback for a free-form mode drag
+ */
+ private void paintFreeFormDropFeedback(IGraphics gc, INode node, DropFeedback feedback,
+ Rect b, GridDropHandler data) {
+ GridModel grid = data.getGrid();
+ if (GridLayoutRule.sSnapToGrid) {
+ GridLayoutPainter.paintGrid(node, gc);
+ }
+ GridLayoutPainter.paintStructure(DrawingStyle.GRID, node, gc, grid);
+
+ GridMatch rowMatch = data.getRowMatch();
+ GridMatch columnMatch = data.getColumnMatch();
+
+ if (rowMatch == null || columnMatch == null) {
+ return;
+ }
+
+ IDragElement first = mElements[0];
+ Rect dragBounds = first.getBounds();
+ int offsetX = 0;
+ int offsetY = 0;
+ if (rowMatch.type == SegmentType.BOTTOM) {
+ offsetY -= dragBounds.h;
+ } else if (rowMatch.type == SegmentType.BASELINE) {
+ offsetY -= feedback.dragBaseline;
+ }
+ if (columnMatch.type == SegmentType.RIGHT) {
+ offsetX -= dragBounds.w;
+ } else if (columnMatch.type == SegmentType.CENTER_HORIZONTAL) {
+ offsetX -= dragBounds.w / 2;
+ }
+
+ // Draw guidelines for matches
+ int y = rowMatch.matchedLine;
+ int x = columnMatch.matchedLine;
+ Rect bounds = first.getBounds();
+
+ // Draw margin
+ if (rowMatch.margin != UNDEFINED && rowMatch.margin > 0) {
+ gc.useStyle(DrawingStyle.DISTANCE);
+ int centerX = bounds.w / 2 + offsetX + x;
+ int y1;
+ int y2;
+ if (rowMatch.type == SegmentType.TOP) {
+ y1 = offsetY + y - 1;
+ y2 = rowMatch.matchedLine - rowMatch.margin;
+ } else {
+ assert rowMatch.type == SegmentType.BOTTOM;
+ y1 = bounds.h + offsetY + y - 1;
+ y2 = rowMatch.matchedLine + rowMatch.margin;
+ }
+ gc.drawLine(b.x, y1, b.x2(), y1);
+ gc.drawLine(b.x, y2, b.x2(), y2);
+ gc.drawString(Integer.toString(rowMatch.margin),
+ centerX - 3, y1 + (y2 - y1 - 16) / 2);
+ } else {
+ gc.useStyle(rowMatch.margin == 0 ? DrawingStyle.DISTANCE
+ : rowMatch.createCell ? DrawingStyle.GUIDELINE_DASHED
+ : DrawingStyle.GUIDELINE);
+ gc.drawLine(b.x, y, b.x2(), y );
+ }
+
+ if (columnMatch.margin != UNDEFINED && columnMatch.margin > 0) {
+ gc.useStyle(DrawingStyle.DISTANCE);
+ int centerY = bounds.h / 2 + offsetY + y;
+ int x1;
+ int x2;
+ if (columnMatch.type == SegmentType.LEFT) {
+ x1 = offsetX + x - 1;
+ x2 = columnMatch.matchedLine - columnMatch.margin;
+ } else {
+ assert columnMatch.type == SegmentType.RIGHT;
+ x1 = bounds.w + offsetX + x - 1;
+ x2 = columnMatch.matchedLine + columnMatch.margin;
+ }
+ gc.drawLine(x1, b.y, x1, b.y2());
+ gc.drawLine(x2, b.y, x2, b.y2());
+ gc.drawString(Integer.toString(columnMatch.margin),
+ x1 + (x2 - x1 - 16) / 2, centerY - 3);
+ } else {
+ gc.useStyle(columnMatch.margin == 0 ? DrawingStyle.DISTANCE
+ : columnMatch.createCell ? DrawingStyle.GUIDELINE_DASHED
+ : DrawingStyle.GUIDELINE);
+ gc.drawLine(x, b.y, x, b.y2());
+ }
+
+ // Draw preview rectangles for all the dragged elements
+ gc.useStyle(DrawingStyle.DROP_PREVIEW);
+ offsetX += x - bounds.x;
+ offsetY += y - bounds.y;
+
+ for (IDragElement element : mElements) {
+ if (element == first) {
+ mRule.drawElement(gc, first, offsetX, offsetY);
+ // Preview baseline as well
+ if (feedback.dragBaseline != -1) {
+ int x1 = dragBounds.x + offsetX;
+ int y1 = dragBounds.y + offsetY + feedback.dragBaseline;
+ gc.drawLine(x1, y1, x1 + dragBounds.w, y1);
+ }
+ } else {
+ b = element.getBounds();
+ if (b.isValid()) {
+ gc.drawRect(b.x + offsetX, b.y + offsetY,
+ b.x + offsetX + b.w, b.y + offsetY + b.h);
+ }
+ }
+ }
+ }
+
+ /**
+ * Paints the drag feedback for a grid-mode drag
+ */
+ private void paintGridModeDropFeedback(IGraphics gc, Rect b, GridDropHandler data) {
+ int radius = mRule.getNewCellSize();
+ GridModel grid = data.getGrid();
+
+ gc.useStyle(DrawingStyle.GUIDELINE);
+ // Paint grid
+ for (int row = 1; row < grid.actualRowCount; row++) {
+ int y = grid.getRowY(row);
+ gc.drawLine(b.x, y - radius, b.x2(), y - radius);
+ gc.drawLine(b.x, y + radius, b.x2(), y + radius);
+
+ }
+ for (int column = 1; column < grid.actualColumnCount; column++) {
+ int x = grid.getColumnX(column);
+ gc.drawLine(x - radius, b.y, x - radius, b.y2());
+ gc.drawLine(x + radius, b.y, x + radius, b.y2());
+ }
+ gc.drawRect(b.x, b.y, b.x2(), b.y2());
+ gc.drawRect(b.x + 2 * radius, b.y + 2 * radius,
+ b.x2() - 2 * radius, b.y2() - 2 * radius);
+
+ GridMatch columnMatch = data.getColumnMatch();
+ GridMatch rowMatch = data.getRowMatch();
+ int column = columnMatch.cellIndex;
+ int row = rowMatch.cellIndex;
+ boolean createColumn = columnMatch.createCell;
+ boolean createRow = rowMatch.createCell;
+
+ Rect cellBounds = grid.getCellBounds(row, column, 1, 1);
+
+ IDragElement first = mElements[0];
+ Rect dragBounds = first.getBounds();
+ int offsetX = cellBounds.x - dragBounds.x;
+ int offsetY = cellBounds.y - dragBounds.y;
+
+ gc.useStyle(DrawingStyle.DROP_ZONE_ACTIVE);
+ if (createColumn) {
+ gc.fillRect(new Rect(cellBounds.x - radius,
+ cellBounds.y + (createRow ? -radius : radius),
+ 2 * radius + 1, cellBounds.h - (createRow ? 0 : 2 * radius)));
+ offsetX -= radius + dragBounds.w / 2;
+ }
+ if (createRow) {
+ gc.fillRect(new Rect(cellBounds.x + radius, cellBounds.y - radius,
+ cellBounds.w - 2 * radius, 2 * radius + 1));
+ offsetY -= radius + dragBounds.h / 2;
+ } else if (!createColumn) {
+ // Choose this cell
+ gc.fillRect(new Rect(cellBounds.x + radius, cellBounds.y + radius,
+ cellBounds.w - 2 * radius, cellBounds.h - 2 * radius));
+ }
+
+ gc.useStyle(DrawingStyle.DROP_PREVIEW);
+
+ Rect bounds = first.getBounds();
+ int x = offsetX;
+ int y = offsetY;
+ if (columnMatch.type == SegmentType.RIGHT) {
+ x += cellBounds.w - bounds.w;
+ } else if (columnMatch.type == SegmentType.CENTER_HORIZONTAL) {
+ x += cellBounds.w / 2 - bounds.w / 2;
+ }
+ if (rowMatch.type == SegmentType.BOTTOM) {
+ y += cellBounds.h - bounds.h;
+ } else if (rowMatch.type == SegmentType.CENTER_VERTICAL) {
+ y += cellBounds.h / 2 - bounds.h / 2;
+ }
+
+ mRule.drawElement(gc, first, x, y);
+ }
+ }
+
+ /**
+ * Paints the structure (the row and column boundaries) of the given
+ * GridLayout
+ *
+ * @param view the instance of the GridLayout whose structure should be
+ * painted
+ * @param style the drawing style to use for the cell boundaries
+ * @param layout the layout element
+ * @param gc the graphics context
+ * @return true if the structure was successfully inferred from the view and
+ * painted
+ */
+ public static boolean paintStructure(Object view, DrawingStyle style, INode layout,
+ IGraphics gc) {
+ Pair<int[],int[]> cellBounds = GridModel.getAxisBounds(view);
+ if (cellBounds != null) {
+ int[] xs = cellBounds.getFirst();
+ int[] ys = cellBounds.getSecond();
+ Rect b = layout.getBounds();
+ gc.useStyle(style);
+ for (int row = 0; row < ys.length; row++) {
+ int y = ys[row] + b.y;
+ gc.drawLine(b.x, y, b.x2(), y);
+ }
+ for (int column = 0; column < xs.length; column++) {
+ int x = xs[column] + b.x;
+ gc.drawLine(x, b.y, x, b.y2());
+ }
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridMatch.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridMatch.java
new file mode 100644
index 000000000..9bee34345
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridMatch.java
@@ -0,0 +1,154 @@
+/*
+ * 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.grid;
+
+import static com.android.ide.common.layout.grid.GridModel.UNDEFINED;
+
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.SegmentType;
+
+/**
+ * A match for a drag within a GridLayout, corresponding to an alignment with another
+ * edge, or a margin, or centering, or a gap distance from another edge and so on.
+ */
+class GridMatch implements Comparable<GridMatch> {
+ /** The distance to the matched edge - used to pick best matches */
+ public final int distance;
+
+ /** Type of edge that was matched (this refers to the edge on the dragged node,
+ * not on the matched node/row/cell etc) */
+ public final SegmentType type;
+
+ /** Row or column for the match */
+ public int cellIndex;
+
+ /** If true, create a new row/column */
+ public boolean createCell;
+
+ /** The actual x or y position of the matched segment */
+ public int matchedLine;
+
+ /** Amount of margin between the matched edges */
+ public int margin;
+
+ /**
+ * Constructs a match.
+ *
+ * @param type the edge of the dragged element that was matched
+ * @param distance the absolute distance from the ideal match - used to find the best
+ * match
+ * @param matchedLine the actual X or Y location of the ideal match
+ * @param cellIndex the index of the row or column we matched with
+ * @param createCell if true, create a new cell by splitting the existing cell at the
+ * matchedLine position
+ * @param margin a margin distance to add to the actual location from the matched line
+ */
+ GridMatch(SegmentType type, int distance, int matchedLine, int cellIndex,
+ boolean createCell, int margin) {
+ super();
+ this.type = type;
+ this.distance = distance;
+ this.matchedLine = matchedLine;
+ this.cellIndex = cellIndex;
+ this.createCell = createCell;
+ this.margin = margin;
+ }
+
+ // Implements Comparable<GridMatch>
+ @Override
+ public int compareTo(GridMatch o) {
+ // Pick closest matches first
+ if (distance != o.distance) {
+ return distance - o.distance;
+ }
+
+ // Prefer some types of matches over other matches
+ return getPriority() - o.getPriority();
+ }
+
+ /**
+ * Describes the match for the user
+ *
+ * @param layout the GridLayout containing the match
+ * @return a short description for the user of the match
+ */
+ public String getDisplayName(INode layout) {
+ switch (type) {
+ case BASELINE:
+ return String.format("Align baseline in row %1$d", cellIndex + 1);
+ case CENTER_HORIZONTAL:
+ return "Center horizontally";
+ case LEFT:
+ if (!createCell) {
+ return String.format("Insert into column %1$d", cellIndex + 1);
+ }
+ if (margin != UNDEFINED) {
+ if (cellIndex == 0 && margin != 0) {
+ return "Add one margin distance from the left";
+ }
+ return String.format("Add next to column %1$d", cellIndex + 1);
+ }
+ return String.format("Align left at x=%1$d", matchedLine - layout.getBounds().x);
+ case RIGHT:
+ if (!createCell) {
+ return String.format("Insert right-aligned into column %1$d", cellIndex + 1);
+ }
+ return String.format("Align right at x=%1$d", matchedLine - layout.getBounds().x);
+ case TOP:
+ if (!createCell) {
+ return String.format("Insert into row %1$d", cellIndex + 1);
+ }
+ if (margin != UNDEFINED) {
+ if (cellIndex == 0 && margin != 0) {
+ return "Add one margin distance from the top";
+ }
+ return String.format("Add below row %1$d", cellIndex + 1);
+ }
+ return String.format("Align top at y=%1d", matchedLine - layout.getBounds().y);
+ case BOTTOM:
+ if (!createCell) {
+ return String.format("Insert into bottom of row %1$d", cellIndex + 1);
+ }
+ return String.format("Align bottom at y=%1d", matchedLine - layout.getBounds().y);
+ case CENTER_VERTICAL:
+ return "Center vertically";
+ case UNKNOWN:
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Computes the sorting priority of this match, giving baseline matches higher
+ * precedence than centering which in turn is ordered before external edge matches
+ */
+ private int getPriority() {
+ switch (type) {
+ case BASELINE:
+ return 0;
+ case CENTER_HORIZONTAL:
+ case CENTER_VERTICAL:
+ return 1;
+ case BOTTOM:
+ case LEFT:
+ case RIGHT:
+ case TOP:
+ return 2;
+ }
+
+ return 3;
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridModel.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridModel.java
new file mode 100644
index 000000000..46770e82c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridModel.java
@@ -0,0 +1,2384 @@
+/*
+ * 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.grid;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_COLUMN_COUNT;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
+import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
+import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
+import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_ORIENTATION;
+import static com.android.SdkConstants.ATTR_ROW_COUNT;
+import static com.android.SdkConstants.FQCN_GRID_LAYOUT;
+import static com.android.SdkConstants.FQCN_SPACE;
+import static com.android.SdkConstants.FQCN_SPACE_V7;
+import static com.android.SdkConstants.GRID_LAYOUT;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.SPACE;
+import static com.android.SdkConstants.VALUE_BOTTOM;
+import static com.android.SdkConstants.VALUE_CENTER_VERTICAL;
+import static com.android.SdkConstants.VALUE_N_DP;
+import static com.android.SdkConstants.VALUE_TOP;
+import static com.android.SdkConstants.VALUE_VERTICAL;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_BOTTOM;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_HORIZ;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_VERT;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_RIGHT;
+import static java.lang.Math.abs;
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.IClientRulesEngine;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewMetadata;
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.layout.GravityHelper;
+import com.android.ide.common.layout.GridLayoutRule;
+import com.android.utils.Pair;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+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.Set;
+
+/** Models a GridLayout */
+public class GridModel {
+ /** Marker value used to indicate values (rows, columns, etc) which have not been set */
+ static final int UNDEFINED = Integer.MIN_VALUE;
+
+ /** The size of spacers in the dimension that they are not defining */
+ static final int SPACER_SIZE_DP = 1;
+
+ /** Attribute value used for {@link #SPACER_SIZE_DP} */
+ private static final String SPACER_SIZE = String.format(VALUE_N_DP, SPACER_SIZE_DP);
+
+ /** Width assigned to a newly added column with the Add Column action */
+ private static final int DEFAULT_CELL_WIDTH = 100;
+
+ /** Height assigned to a newly added row with the Add Row action */
+ private static final int DEFAULT_CELL_HEIGHT = 15;
+
+ /** The GridLayout node, never null */
+ public final INode layout;
+
+ /** True if this is a vertical layout, and false if it is horizontal (the default) */
+ public boolean vertical;
+
+ /** The declared count of rows (which may be {@link #UNDEFINED} if not specified) */
+ public int declaredRowCount;
+
+ /** The declared count of columns (which may be {@link #UNDEFINED} if not specified) */
+ public int declaredColumnCount;
+
+ /** The actual count of rows found in the grid */
+ public int actualRowCount;
+
+ /** The actual count of columns found in the grid */
+ public int actualColumnCount;
+
+ /**
+ * Array of positions (indexed by column) of the left edge of table cells; this
+ * corresponds to the column positions in the grid
+ */
+ private int[] mLeft;
+
+ /**
+ * Array of positions (indexed by row) of the top edge of table cells; this
+ * corresponds to the row positions in the grid
+ */
+ private int[] mTop;
+
+ /**
+ * Array of positions (indexed by column) of the maximum right hand side bounds of a
+ * node in the given column; this represents the visual edge of a column even when the
+ * actual column is wider
+ */
+ private int[] mMaxRight;
+
+ /**
+ * Array of positions (indexed by row) of the maximum bottom bounds of a node in the
+ * given row; this represents the visual edge of a row even when the actual row is
+ * taller
+ */
+ private int[] mMaxBottom;
+
+ /**
+ * Array of baselines computed for the rows. This array is populated lazily and should
+ * not be accessed directly; call {@link #getBaseline(int)} instead.
+ */
+ private int[] mBaselines;
+
+ /** List of all the view data for the children in this layout */
+ private List<ViewData> mChildViews;
+
+ /** The {@link IClientRulesEngine} */
+ private final IClientRulesEngine mRulesEngine;
+
+ /**
+ * An actual instance of a GridLayout object that this grid model corresponds to.
+ */
+ private Object mViewObject;
+
+ /** The namespace to use for attributes */
+ private String mNamespace;
+
+ /**
+ * Constructs a {@link GridModel} for the given layout
+ *
+ * @param rulesEngine the associated rules engine
+ * @param node the GridLayout node
+ * @param viewObject an actual GridLayout instance, or null
+ */
+ private GridModel(IClientRulesEngine rulesEngine, INode node, Object viewObject) {
+ mRulesEngine = rulesEngine;
+ layout = node;
+ mViewObject = viewObject;
+ loadFromXml();
+ }
+
+ // Factory cache for most recent item (used primarily because during paints and drags
+ // the grid model is called repeatedly for the same view object.)
+ private static WeakReference<Object> sCachedViewObject = new WeakReference<Object>(null);
+ private static WeakReference<GridModel> sCachedViewModel;
+
+ /**
+ * Factory which returns a grid model for the given node.
+ *
+ * @param rulesEngine the associated rules engine
+ * @param node the GridLayout node
+ * @param viewObject an actual GridLayout instance, or null
+ * @return a new model
+ */
+ @NonNull
+ public static GridModel get(
+ @NonNull IClientRulesEngine rulesEngine,
+ @NonNull INode node,
+ @Nullable Object viewObject) {
+ if (viewObject != null && viewObject == sCachedViewObject.get()) {
+ GridModel model = sCachedViewModel.get();
+ if (model != null) {
+ return model;
+ }
+ }
+
+ GridModel model = new GridModel(rulesEngine, node, viewObject);
+ sCachedViewModel = new WeakReference<GridModel>(model);
+ sCachedViewObject = new WeakReference<Object>(viewObject);
+ return model;
+ }
+
+ /**
+ * Returns the {@link ViewData} for the child at the given index
+ *
+ * @param index the position of the child node whose view we want to look up
+ * @return the corresponding {@link ViewData}
+ */
+ public ViewData getView(int index) {
+ return mChildViews.get(index);
+ }
+
+ /**
+ * Returns the {@link ViewData} for the given child node.
+ *
+ * @param node the node for which we want the view info
+ * @return the view info for the node, or null if not found
+ */
+ public ViewData getView(INode node) {
+ for (ViewData view : mChildViews) {
+ if (view.node == node) {
+ return view;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Computes the index (among the children nodes) to insert a new node into which
+ * should be positioned at the given row and column. This will skip over any nodes
+ * that have implicit positions earlier than the given node, and will also ensure that
+ * all nodes are placed before the spacer nodes.
+ *
+ * @param row the target row of the new node
+ * @param column the target column of the new node
+ * @return the insert position to use or -1 if no preference is found
+ */
+ public int getInsertIndex(int row, int column) {
+ if (vertical) {
+ for (ViewData view : mChildViews) {
+ if (view.column > column || view.column == column && view.row >= row) {
+ return view.index;
+ }
+ }
+ } else {
+ for (ViewData view : mChildViews) {
+ if (view.row > row || view.row == row && view.column >= column) {
+ return view.index;
+ }
+ }
+ }
+
+ // Place it before the first spacer
+ for (ViewData view : mChildViews) {
+ if (view.isSpacer()) {
+ return view.index;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns the baseline of the given row, or -1 if none is found. This looks for views
+ * in the row which have baseline vertical alignment and also define their own
+ * baseline, and returns the first such match.
+ *
+ * @param row the row to look up a baseline for
+ * @return the baseline relative to the row position, or -1 if not defined
+ */
+ public int getBaseline(int row) {
+ if (row < 0 || row >= mBaselines.length) {
+ return -1;
+ }
+
+ int baseline = mBaselines[row];
+ if (baseline == UNDEFINED) {
+ baseline = -1;
+
+ // TBD: Consider stringing together row information in the view data
+ // so I can quickly identify the views in a given row instead of searching
+ // among all?
+ for (ViewData view : mChildViews) {
+ // We only count baselines for views with rowSpan=1 because
+ // baseline alignment doesn't work for cell spanning views
+ if (view.row == row && view.rowSpan == 1) {
+ baseline = view.node.getBaseline();
+ if (baseline != -1) {
+ // Even views that do have baselines do not count towards a row
+ // baseline if they have a vertical gravity
+ String gravity = getGridAttribute(view.node, ATTR_LAYOUT_GRAVITY);
+ if (gravity == null
+ || !(gravity.contains(VALUE_TOP)
+ || gravity.contains(VALUE_BOTTOM)
+ || gravity.contains(VALUE_CENTER_VERTICAL))) {
+ // Compute baseline relative to the row, not the view itself
+ baseline += view.node.getBounds().y - getRowY(row);
+ break;
+ }
+ }
+ }
+ }
+ mBaselines[row] = baseline;
+ }
+
+ return baseline;
+ }
+
+ /** Applies the row and column values into the XML */
+ void applyPositionAttributes() {
+ for (ViewData view : mChildViews) {
+ view.applyPositionAttributes();
+ }
+
+ // Also fix the columnCount
+ if (getGridAttribute(layout, ATTR_COLUMN_COUNT) != null &&
+ declaredColumnCount > actualColumnCount) {
+ setGridAttribute(layout, ATTR_COLUMN_COUNT, actualColumnCount);
+ }
+ }
+
+ /**
+ * Sets the given GridLayout attribute (rowCount, layout_row, etc) to the
+ * given value. This automatically handles using the right XML namespace
+ * based on whether the GridLayout is the android.widget.GridLayout, or the
+ * support library GridLayout, and whether it's in a library project or not
+ * etc.
+ *
+ * @param node the node to apply the attribute to
+ * @param name the local name of the attribute
+ * @param value the integer value to set the attribute to
+ */
+ public void setGridAttribute(INode node, String name, int value) {
+ setGridAttribute(node, name, Integer.toString(value));
+ }
+
+ /**
+ * Sets the given GridLayout attribute (rowCount, layout_row, etc) to the
+ * given value. This automatically handles using the right XML namespace
+ * based on whether the GridLayout is the android.widget.GridLayout, or the
+ * support library GridLayout, and whether it's in a library project or not
+ * etc.
+ *
+ * @param node the node to apply the attribute to
+ * @param name the local name of the attribute
+ * @param value the string value to set the attribute to, or null to clear
+ * it
+ */
+ public void setGridAttribute(INode node, String name, String value) {
+ node.setAttribute(getNamespace(), name, value);
+ }
+
+ /**
+ * Returns the namespace URI to use for GridLayout-specific attributes, such
+ * as columnCount, layout_column, layout_column_span, layout_gravity etc.
+ *
+ * @return the namespace, never null
+ */
+ public String getNamespace() {
+ if (mNamespace == null) {
+ mNamespace = ANDROID_URI;
+
+ String fqcn = layout.getFqcn();
+ if (!fqcn.equals(GRID_LAYOUT) && !fqcn.equals(FQCN_GRID_LAYOUT)) {
+ mNamespace = mRulesEngine.getAppNameSpace();
+ }
+ }
+
+ return mNamespace;
+ }
+
+ /** Removes the given flag from a flag attribute value and returns the result */
+ static String removeFlag(String flag, String value) {
+ if (value.equals(flag)) {
+ return null;
+ }
+ // Handle spaces between pipes and flag are a prefix, suffix and interior occurrences
+ int index = value.indexOf(flag);
+ if (index != -1) {
+ int pipe = value.lastIndexOf('|', index);
+ int endIndex = index + flag.length();
+ if (pipe != -1) {
+ value = value.substring(0, pipe).trim() + value.substring(endIndex).trim();
+ } else {
+ pipe = value.indexOf('|', endIndex);
+ if (pipe != -1) {
+ value = value.substring(0, index).trim() + value.substring(pipe + 1).trim();
+ } else {
+ value = value.substring(0, index).trim() + value.substring(endIndex).trim();
+ }
+ }
+ }
+
+ return value;
+ }
+
+ /**
+ * Loads a {@link GridModel} from the XML model.
+ */
+ private void loadFromXml() {
+ INode[] children = layout.getChildren();
+
+ declaredRowCount = getGridAttribute(layout, ATTR_ROW_COUNT, UNDEFINED);
+ declaredColumnCount = getGridAttribute(layout, ATTR_COLUMN_COUNT, UNDEFINED);
+ // Horizontal is the default, so if no value is specified it is horizontal.
+ vertical = VALUE_VERTICAL.equals(getGridAttribute(layout, ATTR_ORIENTATION));
+
+ mChildViews = new ArrayList<ViewData>(children.length);
+ int index = 0;
+ for (INode child : children) {
+ ViewData view = new ViewData(child, index++);
+ mChildViews.add(view);
+ }
+
+ // Assign row/column positions to all cells that do not explicitly define them
+ if (!assignRowsAndColumnsFromViews(mChildViews)) {
+ assignRowsAndColumnsFromXml(
+ declaredRowCount == UNDEFINED ? children.length : declaredRowCount,
+ declaredColumnCount == UNDEFINED ? children.length : declaredColumnCount);
+ }
+
+ assignCellBounds();
+
+ for (int i = 0; i <= actualRowCount; i++) {
+ mBaselines[i] = UNDEFINED;
+ }
+ }
+
+ private Pair<Map<Integer, Integer>, Map<Integer, Integer>> findCellsOutsideDeclaredBounds() {
+ // See if we have any (row,column) pairs that fall outside the declared
+ // bounds; for these we identify the number of unique values and assign these
+ // consecutive values
+ Map<Integer, Integer> extraColumnsMap = null;
+ Map<Integer, Integer> extraRowsMap = null;
+ if (declaredRowCount != UNDEFINED) {
+ Set<Integer> extraRows = null;
+ for (ViewData view : mChildViews) {
+ if (view.row >= declaredRowCount) {
+ if (extraRows == null) {
+ extraRows = new HashSet<Integer>();
+ }
+ extraRows.add(view.row);
+ }
+ }
+ if (extraRows != null && declaredRowCount != UNDEFINED) {
+ List<Integer> rows = new ArrayList<Integer>(extraRows);
+ Collections.sort(rows);
+ int row = declaredRowCount;
+ extraRowsMap = new HashMap<Integer, Integer>();
+ for (Integer declared : rows) {
+ extraRowsMap.put(declared, row++);
+ }
+ }
+ }
+ if (declaredColumnCount != UNDEFINED) {
+ Set<Integer> extraColumns = null;
+ for (ViewData view : mChildViews) {
+ if (view.column >= declaredColumnCount) {
+ if (extraColumns == null) {
+ extraColumns = new HashSet<Integer>();
+ }
+ extraColumns.add(view.column);
+ }
+ }
+ if (extraColumns != null && declaredColumnCount != UNDEFINED) {
+ List<Integer> columns = new ArrayList<Integer>(extraColumns);
+ Collections.sort(columns);
+ int column = declaredColumnCount;
+ extraColumnsMap = new HashMap<Integer, Integer>();
+ for (Integer declared : columns) {
+ extraColumnsMap.put(declared, column++);
+ }
+ }
+ }
+
+ return Pair.of(extraRowsMap, extraColumnsMap);
+ }
+
+ /**
+ * Figure out actual row and column numbers for views that do not specify explicit row
+ * and/or column numbers
+ * TODO: Consolidate with the algorithm in GridLayout to ensure we get the
+ * exact same results!
+ */
+ private void assignRowsAndColumnsFromXml(int rowCount, int columnCount) {
+ Pair<Map<Integer, Integer>, Map<Integer, Integer>> p = findCellsOutsideDeclaredBounds();
+ Map<Integer, Integer> extraRowsMap = p.getFirst();
+ Map<Integer, Integer> extraColumnsMap = p.getSecond();
+
+ if (!vertical) {
+ // Horizontal GridLayout: this is the default. Row and column numbers
+ // are assigned by assuming that the children are assigned successive
+ // column numbers until we get to the column count of the grid, at which
+ // point we jump to the next row. If any cell specifies either an explicit
+ // row number of column number, we jump to the next available position.
+ // Note also that if there are any rowspans on the current row, then the
+ // next row we jump to is below the largest such rowspan - in other words,
+ // the algorithm does not fill holes in the middle!
+
+ // TODO: Ensure that we don't run into trouble if a later element specifies
+ // an earlier number... find out what the layout does in that case!
+ int row = 0;
+ int column = 0;
+ int nextRow = 1;
+ for (ViewData view : mChildViews) {
+ int declaredColumn = view.column;
+ if (declaredColumn != UNDEFINED) {
+ if (declaredColumn >= columnCount) {
+ assert extraColumnsMap != null;
+ declaredColumn = extraColumnsMap.get(declaredColumn);
+ view.column = declaredColumn;
+ }
+ if (declaredColumn < column) {
+ // Must jump to the next row to accommodate the new row
+ assert nextRow > row;
+ //row++;
+ row = nextRow;
+ }
+ column = declaredColumn;
+ } else {
+ view.column = column;
+ }
+ if (view.row != UNDEFINED) {
+ // TODO: Should this adjust the column number too? (If so must
+ // also update view.column since we've already processed the local
+ // column number)
+ row = view.row;
+ } else {
+ view.row = row;
+ }
+
+ nextRow = Math.max(nextRow, view.row + view.rowSpan);
+
+ // Advance
+ column += view.columnSpan;
+ if (column >= columnCount) {
+ column = 0;
+ assert nextRow > row;
+ //row++;
+ row = nextRow;
+ }
+ }
+ } else {
+ // Vertical layout: successive children are assigned to the same column in
+ // successive rows.
+ int row = 0;
+ int column = 0;
+ int nextColumn = 1;
+ for (ViewData view : mChildViews) {
+ int declaredRow = view.row;
+ if (declaredRow != UNDEFINED) {
+ if (declaredRow >= rowCount) {
+ declaredRow = extraRowsMap.get(declaredRow);
+ view.row = declaredRow;
+ }
+ if (declaredRow < row) {
+ // Must jump to the next column to accommodate the new column
+ assert nextColumn > column;
+ column = nextColumn;
+ }
+ row = declaredRow;
+ } else {
+ view.row = row;
+ }
+ if (view.column != UNDEFINED) {
+ // TODO: Should this adjust the row number too? (If so must
+ // also update view.row since we've already processed the local
+ // row number)
+ column = view.column;
+ } else {
+ view.column = column;
+ }
+
+ nextColumn = Math.max(nextColumn, view.column + view.columnSpan);
+
+ // Advance
+ row += view.rowSpan;
+ if (row >= rowCount) {
+ row = 0;
+ assert nextColumn > column;
+ //row++;
+ column = nextColumn;
+ }
+ }
+ }
+ }
+
+ private static boolean sAttemptSpecReflection = true;
+
+ private boolean assignRowsAndColumnsFromViews(List<ViewData> views) {
+ if (!sAttemptSpecReflection) {
+ return false;
+ }
+
+ try {
+ // Lazily initialized reflection methods
+ Field spanField = null;
+ Field rowSpecField = null;
+ Field colSpecField = null;
+ Field minField = null;
+ Field maxField = null;
+ Method getLayoutParams = null;
+
+ for (ViewData view : views) {
+ // TODO: If the element *specifies* anything in XML, use that instead
+ Object child = mRulesEngine.getViewObject(view.node);
+ if (child == null) {
+ // Fallback to XML model
+ return false;
+ }
+
+ if (getLayoutParams == null) {
+ getLayoutParams = child.getClass().getMethod("getLayoutParams"); //$NON-NLS-1$
+ }
+ Object layoutParams = getLayoutParams.invoke(child);
+ if (rowSpecField == null) {
+ Class<? extends Object> layoutParamsClass = layoutParams.getClass();
+ rowSpecField = layoutParamsClass.getDeclaredField("rowSpec"); //$NON-NLS-1$
+ colSpecField = layoutParamsClass.getDeclaredField("columnSpec"); //$NON-NLS-1$
+ rowSpecField.setAccessible(true);
+ colSpecField.setAccessible(true);
+ }
+ assert colSpecField != null;
+
+ Object rowSpec = rowSpecField.get(layoutParams);
+ Object colSpec = colSpecField.get(layoutParams);
+ if (spanField == null) {
+ spanField = rowSpec.getClass().getDeclaredField("span"); //$NON-NLS-1$
+ spanField.setAccessible(true);
+ }
+ assert spanField != null;
+ Object rowInterval = spanField.get(rowSpec);
+ Object colInterval = spanField.get(colSpec);
+ if (minField == null) {
+ Class<? extends Object> intervalClass = rowInterval.getClass();
+ minField = intervalClass.getDeclaredField("min"); //$NON-NLS-1$
+ maxField = intervalClass.getDeclaredField("max"); //$NON-NLS-1$
+ minField.setAccessible(true);
+ maxField.setAccessible(true);
+ }
+ assert maxField != null;
+
+ int row = minField.getInt(rowInterval);
+ int col = minField.getInt(colInterval);
+ int rowEnd = maxField.getInt(rowInterval);
+ int colEnd = maxField.getInt(colInterval);
+
+ view.column = col;
+ view.row = row;
+ view.columnSpan = colEnd - col;
+ view.rowSpan = rowEnd - row;
+ }
+
+ return true;
+
+ } catch (Throwable e) {
+ sAttemptSpecReflection = false;
+ return false;
+ }
+ }
+
+ /**
+ * Computes the positions of the column and row boundaries
+ */
+ private void assignCellBounds() {
+ if (!assignCellBoundsFromView()) {
+ assignCellBoundsFromBounds();
+ }
+ initializeMaxBounds();
+ mBaselines = new int[actualRowCount + 1];
+ }
+
+ /**
+ * Computes the positions of the column and row boundaries, using actual
+ * layout data from the associated GridLayout instance (stored in
+ * {@link #mViewObject})
+ */
+ private boolean assignCellBoundsFromView() {
+ if (mViewObject != null) {
+ Pair<int[], int[]> cellBounds = GridModel.getAxisBounds(mViewObject);
+ if (cellBounds != null) {
+ int[] xs = cellBounds.getFirst();
+ int[] ys = cellBounds.getSecond();
+ Rect layoutBounds = layout.getBounds();
+
+ // Handle "blank" grid layouts: insert a fake grid of CELL_COUNT^2 cells
+ // where the user can do initial placement
+ if (actualColumnCount <= 1 && actualRowCount <= 1 && mChildViews.isEmpty()) {
+ final int CELL_COUNT = 1;
+ xs = new int[CELL_COUNT + 1];
+ ys = new int[CELL_COUNT + 1];
+ int cellWidth = layoutBounds.w / CELL_COUNT;
+ int cellHeight = layoutBounds.h / CELL_COUNT;
+
+ for (int i = 0; i <= CELL_COUNT; i++) {
+ xs[i] = i * cellWidth;
+ ys[i] = i * cellHeight;
+ }
+ }
+
+ actualColumnCount = xs.length - 1;
+ actualRowCount = ys.length - 1;
+
+ int layoutBoundsX = layoutBounds.x;
+ int layoutBoundsY = layoutBounds.y;
+ mLeft = new int[xs.length];
+ mTop = new int[ys.length];
+ for (int i = 0; i < xs.length; i++) {
+ mLeft[i] = xs[i] + layoutBoundsX;
+ }
+ for (int i = 0; i < ys.length; i++) {
+ mTop[i] = ys[i] + layoutBoundsY;
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Computes the boundaries of the rows and columns by considering the bounds of the
+ * children.
+ */
+ private void assignCellBoundsFromBounds() {
+ Rect layoutBounds = layout.getBounds();
+
+ // Compute the actualColumnCount and actualRowCount. This -should- be
+ // as easy as declaredColumnCount + extraColumnsMap.size(),
+ // but the user doesn't *have* to declare a column count (or a row count)
+ // and we need both, so go and find the actual row and column maximums.
+ int maxColumn = 0;
+ int maxRow = 0;
+ for (ViewData view : mChildViews) {
+ maxColumn = max(maxColumn, view.column);
+ maxRow = max(maxRow, view.row);
+ }
+ actualColumnCount = maxColumn + 1;
+ actualRowCount = maxRow + 1;
+
+ mLeft = new int[actualColumnCount + 1];
+ for (int i = 1; i < actualColumnCount; i++) {
+ mLeft[i] = UNDEFINED;
+ }
+ mLeft[0] = layoutBounds.x;
+ mLeft[actualColumnCount] = layoutBounds.x2();
+ mTop = new int[actualRowCount + 1];
+ for (int i = 1; i < actualRowCount; i++) {
+ mTop[i] = UNDEFINED;
+ }
+ mTop[0] = layoutBounds.y;
+ mTop[actualRowCount] = layoutBounds.y2();
+
+ for (ViewData view : mChildViews) {
+ Rect bounds = view.node.getBounds();
+ if (!bounds.isValid()) {
+ continue;
+ }
+ int column = view.column;
+ int row = view.row;
+
+ if (mLeft[column] == UNDEFINED) {
+ mLeft[column] = bounds.x;
+ } else {
+ mLeft[column] = Math.min(bounds.x, mLeft[column]);
+ }
+ if (mTop[row] == UNDEFINED) {
+ mTop[row] = bounds.y;
+ } else {
+ mTop[row] = Math.min(bounds.y, mTop[row]);
+ }
+ }
+
+ // Ensure that any empty columns/rows have a valid boundary value; for now,
+ for (int i = actualColumnCount - 1; i >= 0; i--) {
+ if (mLeft[i] == UNDEFINED) {
+ if (i == 0) {
+ mLeft[i] = layoutBounds.x;
+ } else if (i < actualColumnCount - 1) {
+ mLeft[i] = mLeft[i + 1] - 1;
+ if (mLeft[i - 1] != UNDEFINED && mLeft[i] < mLeft[i - 1]) {
+ mLeft[i] = mLeft[i - 1];
+ }
+ } else {
+ mLeft[i] = layoutBounds.x2();
+ }
+ }
+ }
+ for (int i = actualRowCount - 1; i >= 0; i--) {
+ if (mTop[i] == UNDEFINED) {
+ if (i == 0) {
+ mTop[i] = layoutBounds.y;
+ } else if (i < actualRowCount - 1) {
+ mTop[i] = mTop[i + 1] - 1;
+ if (mTop[i - 1] != UNDEFINED && mTop[i] < mTop[i - 1]) {
+ mTop[i] = mTop[i - 1];
+ }
+ } else {
+ mTop[i] = layoutBounds.y2();
+ }
+ }
+ }
+
+ // The bounds should be in ascending order now
+ if (false && GridLayoutRule.sDebugGridLayout) {
+ for (int i = 1; i < actualRowCount; i++) {
+ assert mTop[i + 1] >= mTop[i];
+ }
+ for (int i = 0; i < actualColumnCount; i++) {
+ assert mLeft[i + 1] >= mLeft[i];
+ }
+ }
+ }
+
+ /**
+ * Determine, for each row and column, what the largest x and y edges are
+ * within that row or column. This is used to find a natural split point to
+ * suggest when adding something "to the right of" or "below" another view.
+ */
+ private void initializeMaxBounds() {
+ mMaxRight = new int[actualColumnCount + 1];
+ mMaxBottom = new int[actualRowCount + 1];
+
+ for (ViewData view : mChildViews) {
+ Rect bounds = view.node.getBounds();
+ if (!bounds.isValid()) {
+ continue;
+ }
+
+ if (!view.isSpacer()) {
+ int x2 = bounds.x2();
+ int y2 = bounds.y2();
+ int column = view.column;
+ int row = view.row;
+ int targetColumn = min(actualColumnCount - 1,
+ column + view.columnSpan - 1);
+ int targetRow = min(actualRowCount - 1, row + view.rowSpan - 1);
+ IViewMetadata metadata = mRulesEngine.getMetadata(view.node.getFqcn());
+ if (metadata != null) {
+ Margins insets = metadata.getInsets();
+ if (insets != null) {
+ x2 -= insets.right;
+ y2 -= insets.bottom;
+ }
+ }
+ if (mMaxRight[targetColumn] < x2
+ && ((view.gravity & (GRAVITY_CENTER_HORIZ | GRAVITY_RIGHT)) == 0)) {
+ mMaxRight[targetColumn] = x2;
+ }
+ if (mMaxBottom[targetRow] < y2
+ && ((view.gravity & (GRAVITY_CENTER_VERT | GRAVITY_BOTTOM)) == 0)) {
+ mMaxBottom[targetRow] = y2;
+ }
+ }
+ }
+ }
+
+ /**
+ * Looks up the x[] and y[] locations of the columns and rows in the given GridLayout
+ * instance.
+ *
+ * @param view the GridLayout object, which should already have performed layout
+ * @return a pair of x[] and y[] integer arrays, or null if it could not be found
+ */
+ public static Pair<int[], int[]> getAxisBounds(Object view) {
+ try {
+ Class<?> clz = view.getClass();
+ String verticalAxisName = "verticalAxis";
+ Field horizontalAxis;
+ try {
+ horizontalAxis = clz.getDeclaredField("horizontalAxis"); //$NON-NLS-1$
+ } catch (NoSuchFieldException e) {
+ // Field names changed in KitKat
+ horizontalAxis = clz.getDeclaredField("mHorizontalAxis"); //$NON-NLS-1$
+ verticalAxisName = "mVerticalAxis";
+ }
+ Field verticalAxis = clz.getDeclaredField(verticalAxisName);
+ horizontalAxis.setAccessible(true);
+ verticalAxis.setAccessible(true);
+ Object horizontal = horizontalAxis.get(view);
+ Object vertical = verticalAxis.get(view);
+ Field locations = horizontal.getClass().getDeclaredField("locations"); //$NON-NLS-1$
+ assert locations.getType().isArray() : locations.getType();
+ locations.setAccessible(true);
+ Object horizontalLocations = locations.get(horizontal);
+ Object verticalLocations = locations.get(vertical);
+ int[] xs = (int[]) horizontalLocations;
+ int[] ys = (int[]) verticalLocations;
+ return Pair.of(xs, ys);
+ } catch (Throwable t) {
+ // Probably trying to show a GridLayout on a platform that does not support it.
+ // Return null to indicate that the grid bounds must be computed from view bounds.
+ return null;
+ }
+ }
+
+ /**
+ * Add a new column.
+ *
+ * @param selectedChildren if null or empty, add the column at the end of the grid,
+ * and otherwise add it before the column of the first selected child
+ * @return the newly added column spacer
+ */
+ public INode addColumn(List<? extends INode> selectedChildren) {
+ // Determine insert index
+ int newColumn = actualColumnCount;
+ if (selectedChildren != null && selectedChildren.size() > 0) {
+ INode first = selectedChildren.get(0);
+ ViewData view = getView(first);
+ newColumn = view.column;
+ }
+
+ INode newView = addColumn(newColumn, null, UNDEFINED, false, UNDEFINED, UNDEFINED);
+ if (newView != null) {
+ mRulesEngine.select(Collections.singletonList(newView));
+ }
+
+ return newView;
+ }
+
+ /**
+ * Adds a new column.
+ *
+ * @param newColumn the column index to insert before
+ * @param newView the {@link INode} to insert as the column spacer, which may be null
+ * (in which case a spacer is automatically created)
+ * @param columnWidthDp the width, in device independent pixels, of the column to be
+ * added (which may be {@link #UNDEFINED}
+ * @param split if true, split the existing column into two at the given x position
+ * @param row the row to add the newView to
+ * @param x the x position of the column we're inserting
+ * @return the column spacer
+ */
+ public INode addColumn(int newColumn, INode newView, int columnWidthDp,
+ boolean split, int row, int x) {
+ // Insert a new column
+ actualColumnCount++;
+ if (declaredColumnCount != UNDEFINED) {
+ declaredColumnCount++;
+ setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount);
+ }
+
+ boolean isLastColumn = true;
+ for (ViewData view : mChildViews) {
+ if (view.column >= newColumn) {
+ isLastColumn = false;
+ break;
+ }
+ }
+
+ for (ViewData view : mChildViews) {
+ boolean columnSpanSet = false;
+
+ int endColumn = view.column + view.columnSpan;
+ if (view.column >= newColumn || endColumn == newColumn) {
+ if (view.column == newColumn || endColumn == newColumn) {
+ //if (view.row == 0) {
+ if (newView == null && !isLastColumn) {
+ // Insert a new spacer
+ int index = getChildIndex(layout.getChildren(), view.node);
+ assert view.index == index; // TODO: Get rid of getter
+ if (endColumn == newColumn) {
+ // This cell -ends- at the desired position: insert it after
+ index++;
+ }
+
+ ViewData newViewData = addSpacer(layout, index,
+ split ? row : UNDEFINED,
+ split ? newColumn - 1 : UNDEFINED,
+ columnWidthDp != UNDEFINED ? columnWidthDp : DEFAULT_CELL_WIDTH,
+ DEFAULT_CELL_HEIGHT);
+ newViewData.column = newColumn - 1;
+ newViewData.row = row;
+ newView = newViewData.node;
+ }
+
+ // Set the actual row number on the first cell on the new row.
+ // This means we don't really need the spacer above to imply
+ // the new row number, but we use the spacer to assign the row
+ // some height.
+ if (view.column == newColumn) {
+ view.column++;
+ setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column);
+ } // else: endColumn == newColumn: handled below
+ } else if (getGridAttribute(view.node, ATTR_LAYOUT_COLUMN) != null) {
+ view.column++;
+ setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column);
+ }
+ } else if (endColumn > newColumn) {
+ view.columnSpan++;
+ setColumnSpanAttribute(view.node, view.columnSpan);
+ columnSpanSet = true;
+ }
+
+ if (split && !columnSpanSet && view.node.getBounds().x2() > x) {
+ if (view.node.getBounds().x < x) {
+ view.columnSpan++;
+ setColumnSpanAttribute(view.node, view.columnSpan);
+ }
+ }
+ }
+
+ // Hardcode the row numbers if the last column is a new column such that
+ // they don't jump back to backfill the previous row's new last cell
+ if (isLastColumn) {
+ for (ViewData view : mChildViews) {
+ if (view.column == 0 && view.row > 0) {
+ setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row);
+ }
+ }
+ if (split) {
+ assert newView == null;
+ addSpacer(layout, -1, row, newColumn -1,
+ columnWidthDp != UNDEFINED ? columnWidthDp : DEFAULT_CELL_WIDTH,
+ SPACER_SIZE_DP);
+ }
+ }
+
+ return newView;
+ }
+
+ /**
+ * Removes the columns containing the given selection
+ *
+ * @param selectedChildren a list of nodes whose columns should be deleted
+ */
+ public void removeColumns(List<? extends INode> selectedChildren) {
+ if (selectedChildren.size() == 0) {
+ return;
+ }
+
+ // Figure out which columns should be removed
+ Set<Integer> removeColumns = new HashSet<Integer>();
+ Set<ViewData> removedViews = new HashSet<ViewData>();
+ for (INode child : selectedChildren) {
+ ViewData view = getView(child);
+ removedViews.add(view);
+ removeColumns.add(view.column);
+ }
+ // Sort them in descending order such that we can process each
+ // deletion independently
+ List<Integer> removed = new ArrayList<Integer>(removeColumns);
+ Collections.sort(removed, Collections.reverseOrder());
+
+ for (int removedColumn : removed) {
+ // Remove column.
+ // First, adjust column count.
+ // TODO: Don't do this if the column being deleted is outside
+ // the declared column range!
+ // TODO: Do this under a write lock? / editXml lock?
+ actualColumnCount--;
+ if (declaredColumnCount != UNDEFINED) {
+ declaredColumnCount--;
+ }
+
+ // Remove any elements that begin in the deleted columns...
+ // If they have colspan > 1, then we must insert a spacer instead.
+ // For any other elements that overlap, we need to subtract from the span.
+
+ for (ViewData view : mChildViews) {
+ if (view.column == removedColumn) {
+ int index = getChildIndex(layout.getChildren(), view.node);
+ assert view.index == index; // TODO: Get rid of getter
+ if (view.columnSpan > 1) {
+ // Make a new spacer which is the width of the following
+ // columns
+ int columnWidth = getColumnWidth(removedColumn, view.columnSpan) -
+ getColumnWidth(removedColumn, 1);
+ int columnWidthDip = mRulesEngine.pxToDp(columnWidth);
+ ViewData spacer = addSpacer(layout, index, UNDEFINED, UNDEFINED,
+ columnWidthDip, SPACER_SIZE_DP);
+ spacer.row = 0;
+ spacer.column = removedColumn;
+ }
+ layout.removeChild(view.node);
+ } else if (view.column < removedColumn
+ && view.column + view.columnSpan > removedColumn) {
+ // Subtract column span to skip this item
+ view.columnSpan--;
+ setColumnSpanAttribute(view.node, view.columnSpan);
+ } else if (view.column > removedColumn) {
+ view.column--;
+ if (getGridAttribute(view.node, ATTR_LAYOUT_COLUMN) != null) {
+ setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column);
+ }
+ }
+ }
+ }
+
+ // Remove children from child list!
+ if (removedViews.size() <= 2) {
+ mChildViews.removeAll(removedViews);
+ } else {
+ List<ViewData> remaining =
+ new ArrayList<ViewData>(mChildViews.size() - removedViews.size());
+ for (ViewData view : mChildViews) {
+ if (!removedViews.contains(view)) {
+ remaining.add(view);
+ }
+ }
+ mChildViews = remaining;
+ }
+
+ //if (declaredColumnCount != UNDEFINED) {
+ setGridAttribute(layout, ATTR_COLUMN_COUNT, actualColumnCount);
+ //}
+
+ }
+
+ /**
+ * Add a new row.
+ *
+ * @param selectedChildren if null or empty, add the row at the bottom of the grid,
+ * and otherwise add it before the row of the first selected child
+ * @return the newly added row spacer
+ */
+ public INode addRow(List<? extends INode> selectedChildren) {
+ // Determine insert index
+ int newRow = actualRowCount;
+ if (selectedChildren.size() > 0) {
+ INode first = selectedChildren.get(0);
+ ViewData view = getView(first);
+ newRow = view.row;
+ }
+
+ INode newView = addRow(newRow, null, UNDEFINED, false, UNDEFINED, UNDEFINED);
+ if (newView != null) {
+ mRulesEngine.select(Collections.singletonList(newView));
+ }
+
+ return newView;
+ }
+
+ /**
+ * Adds a new column.
+ *
+ * @param newRow the row index to insert before
+ * @param newView the {@link INode} to insert as the row spacer, which may be null (in
+ * which case a spacer is automatically created)
+ * @param rowHeightDp the height, in device independent pixels, of the row to be added
+ * (which may be {@link #UNDEFINED}
+ * @param split if true, split the existing row into two at the given y position
+ * @param column the column to add the newView to
+ * @param y the y position of the row we're inserting
+ * @return the row spacer
+ */
+ public INode addRow(int newRow, INode newView, int rowHeightDp, boolean split,
+ int column, int y) {
+ actualRowCount++;
+ if (declaredRowCount != UNDEFINED) {
+ declaredRowCount++;
+ setGridAttribute(layout, ATTR_ROW_COUNT, declaredRowCount);
+ }
+
+ boolean added = false;
+ for (ViewData view : mChildViews) {
+ if (view.row >= newRow) {
+ // Adjust the column count
+ if (view.row == newRow && view.column == 0) {
+ // Insert a new spacer
+ if (newView == null) {
+ int index = getChildIndex(layout.getChildren(), view.node);
+ assert view.index == index; // TODO: Get rid of getter
+ if (declaredColumnCount != UNDEFINED && !split) {
+ setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount);
+ }
+ ViewData newViewData = addSpacer(layout, index,
+ split ? newRow - 1 : UNDEFINED,
+ split ? column : UNDEFINED,
+ SPACER_SIZE_DP,
+ rowHeightDp != UNDEFINED ? rowHeightDp : DEFAULT_CELL_HEIGHT);
+ newViewData.column = column;
+ newViewData.row = newRow - 1;
+ newView = newViewData.node;
+ }
+
+ // Set the actual row number on the first cell on the new row.
+ // This means we don't really need the spacer above to imply
+ // the new row number, but we use the spacer to assign the row
+ // some height.
+ view.row++;
+ setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row);
+
+ added = true;
+ } else if (getGridAttribute(view.node, ATTR_LAYOUT_ROW) != null) {
+ view.row++;
+ setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row);
+ }
+ } else {
+ int endRow = view.row + view.rowSpan;
+ if (endRow > newRow) {
+ view.rowSpan++;
+ setRowSpanAttribute(view.node, view.rowSpan);
+ } else if (split && view.node.getBounds().y2() > y) {
+ if (view.node.getBounds().y < y) {
+ view.rowSpan++;
+ setRowSpanAttribute(view.node, view.rowSpan);
+ }
+ }
+ }
+ }
+
+ if (!added) {
+ // Append a row at the end
+ if (newView == null) {
+ ViewData newViewData = addSpacer(layout, -1, UNDEFINED, UNDEFINED,
+ SPACER_SIZE_DP,
+ rowHeightDp != UNDEFINED ? rowHeightDp : DEFAULT_CELL_HEIGHT);
+ newViewData.column = column;
+ // TODO: MAke sure this row number is right!
+ newViewData.row = split ? newRow - 1 : newRow;
+ newView = newViewData.node;
+ }
+ if (declaredColumnCount != UNDEFINED && !split) {
+ setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount);
+ }
+ if (split) {
+ setGridAttribute(newView, ATTR_LAYOUT_ROW, newRow - 1);
+ setGridAttribute(newView, ATTR_LAYOUT_COLUMN, column);
+ }
+ }
+
+ return newView;
+ }
+
+ /**
+ * Removes the rows containing the given selection
+ *
+ * @param selectedChildren a list of nodes whose rows should be deleted
+ */
+ public void removeRows(List<? extends INode> selectedChildren) {
+ if (selectedChildren.size() == 0) {
+ return;
+ }
+
+ // Figure out which rows should be removed
+ Set<ViewData> removedViews = new HashSet<ViewData>();
+ Set<Integer> removedRows = new HashSet<Integer>();
+ for (INode child : selectedChildren) {
+ ViewData view = getView(child);
+ removedViews.add(view);
+ removedRows.add(view.row);
+ }
+ // Sort them in descending order such that we can process each
+ // deletion independently
+ List<Integer> removed = new ArrayList<Integer>(removedRows);
+ Collections.sort(removed, Collections.reverseOrder());
+
+ for (int removedRow : removed) {
+ // Remove row.
+ // First, adjust row count.
+ // TODO: Don't do this if the row being deleted is outside
+ // the declared row range!
+ actualRowCount--;
+ if (declaredRowCount != UNDEFINED) {
+ declaredRowCount--;
+ setGridAttribute(layout, ATTR_ROW_COUNT, declaredRowCount);
+ }
+
+ // Remove any elements that begin in the deleted rows...
+ // If they have colspan > 1, then we must hardcode a new row number
+ // instead.
+ // For any other elements that overlap, we need to subtract from the span.
+
+ for (ViewData view : mChildViews) {
+ if (view.row == removedRow) {
+ // We don't have to worry about a rowSpan > 1 here, because even
+ // if it is, those rowspans are not used to assign default row/column
+ // positions for other cells
+// TODO: Check this; it differs from the removeColumns logic!
+ layout.removeChild(view.node);
+ } else if (view.row > removedRow) {
+ view.row--;
+ if (getGridAttribute(view.node, ATTR_LAYOUT_ROW) != null) {
+ setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row);
+ }
+ } else if (view.row < removedRow
+ && view.row + view.rowSpan > removedRow) {
+ // Subtract row span to skip this item
+ view.rowSpan--;
+ setRowSpanAttribute(view.node, view.rowSpan);
+ }
+ }
+ }
+
+ // Remove children from child list!
+ if (removedViews.size() <= 2) {
+ mChildViews.removeAll(removedViews);
+ } else {
+ List<ViewData> remaining =
+ new ArrayList<ViewData>(mChildViews.size() - removedViews.size());
+ for (ViewData view : mChildViews) {
+ if (!removedViews.contains(view)) {
+ remaining.add(view);
+ }
+ }
+ mChildViews = remaining;
+ }
+ }
+
+ /**
+ * Returns the row containing the given y line
+ *
+ * @param y the vertical position
+ * @return the row containing the given line
+ */
+ public int getRow(int y) {
+ int row = Arrays.binarySearch(mTop, y);
+ if (row == -1) {
+ // Smaller than the first element; just use the first row
+ return 0;
+ } else if (row < 0) {
+ row = -(row + 2);
+ }
+
+ return row;
+ }
+
+ /**
+ * Returns the column containing the given x line
+ *
+ * @param x the horizontal position
+ * @return the column containing the given line
+ */
+ public int getColumn(int x) {
+ int column = Arrays.binarySearch(mLeft, x);
+ if (column == -1) {
+ // Smaller than the first element; just use the first column
+ return 0;
+ } else if (column < 0) {
+ column = -(column + 2);
+ }
+
+ return column;
+ }
+
+ /**
+ * Returns the closest row to the given y line. This is
+ * either the row containing the line, or the row below it.
+ *
+ * @param y the vertical position
+ * @return the closest row
+ */
+ public int getClosestRow(int y) {
+ int row = Arrays.binarySearch(mTop, y);
+ if (row == -1) {
+ // Smaller than the first element; just use the first column
+ return 0;
+ } else if (row < 0) {
+ row = -(row + 2);
+ }
+
+ if (getRowDistance(row, y) < getRowDistance(row + 1, y)) {
+ return row;
+ } else {
+ return row + 1;
+ }
+ }
+
+ /**
+ * Returns the closest column to the given x line. This is
+ * either the column containing the line, or the column following it.
+ *
+ * @param x the horizontal position
+ * @return the closest column
+ */
+ public int getClosestColumn(int x) {
+ int column = Arrays.binarySearch(mLeft, x);
+ if (column == -1) {
+ // Smaller than the first element; just use the first column
+ return 0;
+ } else if (column < 0) {
+ column = -(column + 2);
+ }
+
+ if (getColumnDistance(column, x) < getColumnDistance(column + 1, x)) {
+ return column;
+ } else {
+ return column + 1;
+ }
+ }
+
+ /**
+ * Returns the distance between the given x position and the beginning of the given column
+ *
+ * @param column the column
+ * @param x the x position
+ * @return the distance between the two
+ */
+ public int getColumnDistance(int column, int x) {
+ return abs(getColumnX(column) - x);
+ }
+
+ /**
+ * Returns the actual width of the given column. This returns the difference between
+ * the rightmost edge of the views (not including spacers) and the left edge of the
+ * column.
+ *
+ * @param column the column
+ * @return the actual width of the non-spacer views in the column
+ */
+ public int getColumnActualWidth(int column) {
+ return getColumnMaxX(column) - getColumnX(column);
+ }
+
+ /**
+ * Returns the distance between the given y position and the top of the given row
+ *
+ * @param row the row
+ * @param y the y position
+ * @return the distance between the two
+ */
+ public int getRowDistance(int row, int y) {
+ return abs(getRowY(row) - y);
+ }
+
+ /**
+ * Returns the y position of the top of the given row
+ *
+ * @param row the target row
+ * @return the y position of its top edge
+ */
+ public int getRowY(int row) {
+ return mTop[min(mTop.length - 1, max(0, row))];
+ }
+
+ /**
+ * Returns the bottom-most edge of any of the non-spacer children in the given row
+ *
+ * @param row the target row
+ * @return the bottom-most edge of any of the non-spacer children in the row
+ */
+ public int getRowMaxY(int row) {
+ return mMaxBottom[min(mMaxBottom.length - 1, max(0, row))];
+ }
+
+ /**
+ * Returns the actual height of the given row. This returns the difference between
+ * the bottom-most edge of the views (not including spacers) and the top edge of the
+ * row.
+ *
+ * @param row the row
+ * @return the actual height of the non-spacer views in the row
+ */
+ public int getRowActualHeight(int row) {
+ return getRowMaxY(row) - getRowY(row);
+ }
+
+ /**
+ * Returns a list of all the nodes that intersects the rows in the range
+ * {@code y1 <= y <= y2}.
+ *
+ * @param y1 the starting y, inclusive
+ * @param y2 the ending y, inclusive
+ * @return a list of nodes intersecting the given rows, never null but possibly empty
+ */
+ public Collection<INode> getIntersectsRow(int y1, int y2) {
+ List<INode> nodes = new ArrayList<INode>();
+
+ for (ViewData view : mChildViews) {
+ if (!view.isSpacer()) {
+ Rect bounds = view.node.getBounds();
+ if (bounds.y2() >= y1 && bounds.y <= y2) {
+ nodes.add(view.node);
+ }
+ }
+ }
+
+ return nodes;
+ }
+
+ /**
+ * Returns the height of the given row or rows (if the rowSpan is greater than 1)
+ *
+ * @param row the target row
+ * @param rowSpan the row span
+ * @return the height in pixels of the given rows
+ */
+ public int getRowHeight(int row, int rowSpan) {
+ return getRowY(row + rowSpan) - getRowY(row);
+ }
+
+ /**
+ * Returns the x position of the left edge of the given column
+ *
+ * @param column the target column
+ * @return the x position of its left edge
+ */
+ public int getColumnX(int column) {
+ return mLeft[min(mLeft.length - 1, max(0, column))];
+ }
+
+ /**
+ * Returns the rightmost edge of any of the non-spacer children in the given row
+ *
+ * @param column the target column
+ * @return the rightmost edge of any of the non-spacer children in the column
+ */
+ public int getColumnMaxX(int column) {
+ return mMaxRight[min(mMaxRight.length - 1, max(0, column))];
+ }
+
+ /**
+ * Returns the width of the given column or columns (if the columnSpan is greater than 1)
+ *
+ * @param column the target column
+ * @param columnSpan the column span
+ * @return the width in pixels of the given columns
+ */
+ public int getColumnWidth(int column, int columnSpan) {
+ return getColumnX(column + columnSpan) - getColumnX(column);
+ }
+
+ /**
+ * Returns the bounds of the cell at the given row and column position, with the given
+ * row and column spans.
+ *
+ * @param row the target row
+ * @param column the target column
+ * @param rowSpan the row span
+ * @param columnSpan the column span
+ * @return the bounds, in pixels, of the given cell
+ */
+ public Rect getCellBounds(int row, int column, int rowSpan, int columnSpan) {
+ return new Rect(getColumnX(column), getRowY(row),
+ getColumnWidth(column, columnSpan),
+ getRowHeight(row, rowSpan));
+ }
+
+ /**
+ * Produces a display of view contents along with the pixel positions of each
+ * row/column, like the following (used for diagnostics only)
+ *
+ * <pre>
+ * |0 |49 |143 |192 |240
+ * 36| | |button2 |
+ * 72| |radioButton1 |button2 |
+ * 74|button1 |radioButton1 |button2 |
+ * 108|button1 | |button2 |
+ * 110| | |button2 |
+ * 149| | | |
+ * 320
+ * </pre>
+ */
+ @Override
+ public String toString() {
+ // Dump out the view table
+ int cellWidth = 25;
+
+ List<List<List<ViewData>>> rowList = new ArrayList<List<List<ViewData>>>(mTop.length);
+ for (int row = 0; row < mTop.length; row++) {
+ List<List<ViewData>> columnList = new ArrayList<List<ViewData>>(mLeft.length);
+ for (int col = 0; col < mLeft.length; col++) {
+ columnList.add(new ArrayList<ViewData>(4));
+ }
+ rowList.add(columnList);
+ }
+ for (ViewData view : mChildViews) {
+ for (int i = 0; i < view.rowSpan; i++) {
+ if (view.row + i > mTop.length) { // Guard against bogus span values
+ break;
+ }
+ if (rowList.size() <= view.row + i) {
+ break;
+ }
+ for (int j = 0; j < view.columnSpan; j++) {
+ List<List<ViewData>> columnList = rowList.get(view.row + i);
+ if (columnList.size() <= view.column + j) {
+ break;
+ }
+ columnList.get(view.column + j).add(view);
+ }
+ }
+ }
+
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter out = new PrintWriter(stringWriter);
+ out.printf("%" + cellWidth + "s", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ for (int col = 0; col < actualColumnCount + 1; col++) {
+ out.printf("|%-" + (cellWidth - 1) + "d", mLeft[col]); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ out.printf("\n"); //$NON-NLS-1$
+ for (int row = 0; row < actualRowCount + 1; row++) {
+ out.printf("%" + cellWidth + "d", mTop[row]); //$NON-NLS-1$ //$NON-NLS-2$
+ if (row == actualRowCount) {
+ break;
+ }
+ for (int col = 0; col < actualColumnCount; col++) {
+ List<ViewData> views = rowList.get(row).get(col);
+
+ StringBuilder sb = new StringBuilder();
+ for (ViewData view : views) {
+ String id = view != null ? view.getId() : ""; //$NON-NLS-1$
+ if (id.startsWith(NEW_ID_PREFIX)) {
+ id = id.substring(NEW_ID_PREFIX.length());
+ }
+ if (id.length() > cellWidth - 2) {
+ id = id.substring(0, cellWidth - 2);
+ }
+ if (sb.length() > 0) {
+ sb.append(',');
+ }
+ sb.append(id);
+ }
+ String cellString = sb.toString();
+ if (cellString.contains(",") && cellString.length() > cellWidth - 2) { //$NON-NLS-1$
+ cellString = cellString.substring(0, cellWidth - 6) + "...,"; //$NON-NLS-1$
+ }
+ out.printf("|%-" + (cellWidth - 2) + "s ", cellString); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ out.printf("\n"); //$NON-NLS-1$
+ }
+
+ out.flush();
+ return stringWriter.toString();
+ }
+
+ /**
+ * Split a cell into two or three columns.
+ *
+ * @param newColumn The column number to insert before
+ * @param insertMarginColumn If false, then the cell at newColumn -1 is split with the
+ * left part taking up exactly columnWidthDp dips. If true, then the column
+ * is split twice; the left part is the implicit width of the column, the
+ * new middle (margin) column is exactly the columnWidthDp size and the
+ * right column is the remaining space of the old cell.
+ * @param columnWidthDp The width of the column inserted before the new column (or if
+ * insertMarginColumn is false, then the width of the margin column)
+ * @param x the x coordinate of the new column
+ */
+ public void splitColumn(int newColumn, boolean insertMarginColumn, int columnWidthDp, int x) {
+ actualColumnCount++;
+
+ // Insert a new column
+ if (declaredColumnCount != UNDEFINED) {
+ declaredColumnCount++;
+ if (insertMarginColumn) {
+ declaredColumnCount++;
+ }
+ setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount);
+ }
+
+ // Are we inserting a new last column in the grid? That requires some special handling...
+ boolean isLastColumn = true;
+ for (ViewData view : mChildViews) {
+ if (view.column >= newColumn) {
+ isLastColumn = false;
+ break;
+ }
+ }
+
+ // Hardcode the row numbers if the last column is a new column such that
+ // they don't jump back to backfill the previous row's new last cell:
+ // TODO: Only do this for horizontal layouts!
+ if (isLastColumn) {
+ for (ViewData view : mChildViews) {
+ if (view.column == 0 && view.row > 0) {
+ if (getGridAttribute(view.node, ATTR_LAYOUT_ROW) == null) {
+ setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row);
+ }
+ }
+ }
+ }
+
+ // Find the spacer which marks this column, and if found, mark it as a split
+ ViewData prevColumnSpacer = null;
+ for (ViewData view : mChildViews) {
+ if (view.column == newColumn - 1 && view.isColumnSpacer()) {
+ prevColumnSpacer = view;
+ break;
+ }
+ }
+
+ // Process all existing grid elements:
+ // * Increase column numbers for all columns that have a hardcoded column number
+ // greater than the new column
+ // * Set an explicit column=0 where needed (TODO: Implement this)
+ // * Increase the columnSpan for all columns that overlap the newly inserted column edge
+ // * Split the spacer which defined the size of this column into two
+ // (and if not found, create a new spacer)
+ //
+ for (ViewData view : mChildViews) {
+ if (view == prevColumnSpacer) {
+ continue;
+ }
+
+ INode node = view.node;
+ int column = view.column;
+ if (column > newColumn || (column == newColumn && view.node.getBounds().x2() > x)) {
+ // ALWAYS set the column, because
+ // (1) if it has been set, it needs to be corrected
+ // (2) if it has not been set, it needs to be set to cause this column
+ // to skip over the new column (there may be no views for the new
+ // column on this row).
+ // TODO: Enhance this such that we only set the column to a skip number
+ // where necessary, e.g. only on the FIRST view on this row following the
+ // skipped column!
+
+ //if (getGridAttribute(node, ATTR_LAYOUT_COLUMN) != null) {
+ view.column += insertMarginColumn ? 2 : 1;
+ setGridAttribute(node, ATTR_LAYOUT_COLUMN, view.column);
+ //}
+ } else if (!view.isSpacer()) {
+ // Adjust the column span? We must increase it if
+ // (1) the new column is inside the range [column, column + columnSpan]
+ // (2) the new column is within the last cell in the column span,
+ // and the exact X location of the split is within the horizontal
+ // *bounds* of this node (provided it has gravity=left)
+ // (3) the new column is within the last cell and the cell has gravity
+ // right or gravity center
+ int endColumn = column + view.columnSpan;
+ if (endColumn > newColumn
+ || endColumn == newColumn && (view.node.getBounds().x2() > x
+ || GravityHelper.isConstrainedHorizontally(view.gravity)
+ && !GravityHelper.isLeftAligned(view.gravity))) {
+ // This cell spans the new insert position, so increment the column span
+ view.columnSpan += insertMarginColumn ? 2 : 1;
+ setColumnSpanAttribute(node, view.columnSpan);
+ }
+ }
+ }
+
+ // Insert new spacer:
+ if (prevColumnSpacer != null) {
+ int px = getColumnWidth(newColumn - 1, 1);
+ if (insertMarginColumn || columnWidthDp == 0) {
+ px -= getColumnActualWidth(newColumn - 1);
+ }
+ int dp = mRulesEngine.pxToDp(px);
+ int remaining = dp - columnWidthDp;
+ if (remaining > 0) {
+ prevColumnSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH,
+ String.format(VALUE_N_DP, remaining));
+ prevColumnSpacer.column = insertMarginColumn ? newColumn + 1 : newColumn;
+ setGridAttribute(prevColumnSpacer.node, ATTR_LAYOUT_COLUMN,
+ prevColumnSpacer.column);
+ }
+ }
+
+ if (columnWidthDp > 0) {
+ int index = prevColumnSpacer != null ? prevColumnSpacer.index : -1;
+
+ addSpacer(layout, index, 0, insertMarginColumn ? newColumn : newColumn - 1,
+ columnWidthDp, SPACER_SIZE_DP);
+ }
+ }
+
+ /**
+ * Split a cell into two or three rows.
+ *
+ * @param newRow The row number to insert before
+ * @param insertMarginRow If false, then the cell at newRow -1 is split with the above
+ * part taking up exactly rowHeightDp dips. If true, then the row is split
+ * twice; the top part is the implicit height of the row, the new middle
+ * (margin) row is exactly the rowHeightDp size and the bottom column is
+ * the remaining space of the old cell.
+ * @param rowHeightDp The height of the row inserted before the new row (or if
+ * insertMarginRow is false, then the height of the margin row)
+ * @param y the y coordinate of the new row
+ */
+ public void splitRow(int newRow, boolean insertMarginRow, int rowHeightDp, int y) {
+ actualRowCount++;
+
+ // Insert a new row
+ if (declaredRowCount != UNDEFINED) {
+ declaredRowCount++;
+ if (insertMarginRow) {
+ declaredRowCount++;
+ }
+ setGridAttribute(layout, ATTR_ROW_COUNT, declaredRowCount);
+ }
+
+ // Find the spacer which marks this row, and if found, mark it as a split
+ ViewData prevRowSpacer = null;
+ for (ViewData view : mChildViews) {
+ if (view.row == newRow - 1 && view.isRowSpacer()) {
+ prevRowSpacer = view;
+ break;
+ }
+ }
+
+ // Se splitColumn() for details
+ for (ViewData view : mChildViews) {
+ if (view == prevRowSpacer) {
+ continue;
+ }
+
+ INode node = view.node;
+ int row = view.row;
+ if (row > newRow || (row == newRow && view.node.getBounds().y2() > y)) {
+ //if (getGridAttribute(node, ATTR_LAYOUT_ROW) != null) {
+ view.row += insertMarginRow ? 2 : 1;
+ setGridAttribute(node, ATTR_LAYOUT_ROW, view.row);
+ //}
+ } else if (!view.isSpacer()) {
+ int endRow = row + view.rowSpan;
+ if (endRow > newRow
+ || endRow == newRow && (view.node.getBounds().y2() > y
+ || GravityHelper.isConstrainedVertically(view.gravity)
+ && !GravityHelper.isTopAligned(view.gravity))) {
+ // This cell spans the new insert position, so increment the row span
+ view.rowSpan += insertMarginRow ? 2 : 1;
+ setRowSpanAttribute(node, view.rowSpan);
+ }
+ }
+ }
+
+ // Insert new spacer:
+ if (prevRowSpacer != null) {
+ int px = getRowHeight(newRow - 1, 1);
+ if (insertMarginRow || rowHeightDp == 0) {
+ px -= getRowActualHeight(newRow - 1);
+ }
+ int dp = mRulesEngine.pxToDp(px);
+ int remaining = dp - rowHeightDp;
+ if (remaining > 0) {
+ prevRowSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT,
+ String.format(VALUE_N_DP, remaining));
+ prevRowSpacer.row = insertMarginRow ? newRow + 1 : newRow;
+ setGridAttribute(prevRowSpacer.node, ATTR_LAYOUT_ROW, prevRowSpacer.row);
+ }
+ }
+
+ if (rowHeightDp > 0) {
+ int index = prevRowSpacer != null ? prevRowSpacer.index : -1;
+ addSpacer(layout, index, insertMarginRow ? newRow : newRow - 1,
+ 0, SPACER_SIZE_DP, rowHeightDp);
+ }
+ }
+
+ /**
+ * Data about a view in a table; this is not the same as a cell because multiple views
+ * can share a single cell, and a view can span many cells.
+ */
+ public class ViewData {
+ public final INode node;
+ public final int index;
+ public int row;
+ public int column;
+ public int rowSpan;
+ public int columnSpan;
+ public int gravity;
+
+ ViewData(INode n, int index) {
+ node = n;
+ this.index = index;
+
+ column = getGridAttribute(n, ATTR_LAYOUT_COLUMN, UNDEFINED);
+ columnSpan = getGridAttribute(n, ATTR_LAYOUT_COLUMN_SPAN, 1);
+ row = getGridAttribute(n, ATTR_LAYOUT_ROW, UNDEFINED);
+ rowSpan = getGridAttribute(n, ATTR_LAYOUT_ROW_SPAN, 1);
+ gravity = GravityHelper.getGravity(getGridAttribute(n, ATTR_LAYOUT_GRAVITY), 0);
+ }
+
+ /** Applies the column and row fields into the XML model */
+ void applyPositionAttributes() {
+ setGridAttribute(node, ATTR_LAYOUT_COLUMN, column);
+ setGridAttribute(node, ATTR_LAYOUT_ROW, row);
+ }
+
+ /** Returns the id of this node, or makes one up for display purposes */
+ String getId() {
+ String id = node.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id == null) {
+ id = "<unknownid>"; //$NON-NLS-1$
+ String fqn = node.getFqcn();
+ fqn = fqn.substring(fqn.lastIndexOf('.') + 1);
+ id = fqn + "-"
+ + Integer.toString(System.identityHashCode(node)).substring(0, 3);
+ }
+
+ return id;
+ }
+
+ /** Returns true if this {@link ViewData} represents a spacer */
+ boolean isSpacer() {
+ return isSpace(node.getFqcn());
+ }
+
+ /**
+ * Returns true if this {@link ViewData} represents a column spacer
+ */
+ boolean isColumnSpacer() {
+ return isSpacer() &&
+ // Any spacer not found in column 0 is a column spacer since we
+ // place all horizontal spacers in column 0
+ ((column > 0)
+ // TODO: Find a cleaner way. Maybe set ids on the elements in (0,0) and
+ // for column distinguish by id. Or at least only do this for column 0!
+ || !SPACER_SIZE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WIDTH)));
+ }
+
+ /**
+ * Returns true if this {@link ViewData} represents a row spacer
+ */
+ boolean isRowSpacer() {
+ return isSpacer() &&
+ // Any spacer not found in row 0 is a row spacer since we
+ // place all vertical spacers in row 0
+ ((row > 0)
+ // TODO: Find a cleaner way. Maybe set ids on the elements in (0,0) and
+ // for column distinguish by id. Or at least only do this for column 0!
+ || !SPACER_SIZE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_HEIGHT)));
+ }
+ }
+
+ /**
+ * Sets the column span of the given node to the given value (or if the value is 1,
+ * removes it)
+ *
+ * @param node the target node
+ * @param span the new column span
+ */
+ public void setColumnSpanAttribute(INode node, int span) {
+ setGridAttribute(node, ATTR_LAYOUT_COLUMN_SPAN, span > 1 ? Integer.toString(span) : null);
+ }
+
+ /**
+ * Sets the row span of the given node to the given value (or if the value is 1,
+ * removes it)
+ *
+ * @param node the target node
+ * @param span the new row span
+ */
+ public void setRowSpanAttribute(INode node, int span) {
+ setGridAttribute(node, ATTR_LAYOUT_ROW_SPAN, span > 1 ? Integer.toString(span) : null);
+ }
+
+ /** Returns the index of the given target node in the given child node array */
+ static int getChildIndex(INode[] children, INode target) {
+ int index = 0;
+ for (INode child : children) {
+ if (child == target) {
+ return index;
+ }
+ index++;
+ }
+
+ return -1;
+ }
+
+ /**
+ * Update the model to account for the given nodes getting deleted. The nodes
+ * are not actually deleted by this method; that is assumed to be performed by the
+ * caller. Instead this method performs whatever model updates are necessary to
+ * preserve the grid structure.
+ *
+ * @param nodes the nodes to be deleted
+ */
+ public void onDeleted(@NonNull List<INode> nodes) {
+ if (nodes.size() == 0) {
+ return;
+ }
+
+ // Attempt to clean up spacer objects for any newly-empty rows or columns
+ // as the result of this deletion
+
+ Set<INode> deleted = new HashSet<INode>();
+
+ for (INode child : nodes) {
+ // We don't care about deletion of spacers
+ String fqcn = child.getFqcn();
+ if (fqcn.equals(FQCN_SPACE) || fqcn.equals(FQCN_SPACE_V7)) {
+ continue;
+ }
+ deleted.add(child);
+ }
+
+ Set<Integer> usedColumns = new HashSet<Integer>(actualColumnCount);
+ Set<Integer> usedRows = new HashSet<Integer>(actualRowCount);
+ Multimap<Integer, ViewData> columnSpacers = ArrayListMultimap.create(actualColumnCount, 2);
+ Multimap<Integer, ViewData> rowSpacers = ArrayListMultimap.create(actualRowCount, 2);
+ Set<ViewData> removedViews = new HashSet<ViewData>();
+
+ for (ViewData view : mChildViews) {
+ if (deleted.contains(view.node)) {
+ removedViews.add(view);
+ } else if (view.isColumnSpacer()) {
+ columnSpacers.put(view.column, view);
+ } else if (view.isRowSpacer()) {
+ rowSpacers.put(view.row, view);
+ } else {
+ usedColumns.add(Integer.valueOf(view.column));
+ usedRows.add(Integer.valueOf(view.row));
+ }
+ }
+
+ if (usedColumns.size() == 0 || usedRows.size() == 0) {
+ // No more views - just remove all the spacers
+ for (ViewData spacer : columnSpacers.values()) {
+ layout.removeChild(spacer.node);
+ }
+ for (ViewData spacer : rowSpacers.values()) {
+ layout.removeChild(spacer.node);
+ }
+ mChildViews.clear();
+ actualColumnCount = 0;
+ declaredColumnCount = 2;
+ actualRowCount = 0;
+ declaredRowCount = UNDEFINED;
+ setGridAttribute(layout, ATTR_COLUMN_COUNT, 2);
+
+ return;
+ }
+
+ // Determine columns to introduce spacers into:
+ // This is tricky; I should NOT combine spacers if there are cells tied to
+ // individual ones
+
+ // TODO: Invalidate column sizes too! Otherwise repeated updates might get confused!
+ // Similarly, inserts need to do the same!
+
+ // Produce map of old column numbers to new column numbers
+ // Collapse regions of consecutive space and non-space ranges together
+ int[] columnMap = new int[actualColumnCount + 1]; // +1: Easily handle columnSpans as well
+ int newColumn = 0;
+ boolean prevUsed = usedColumns.contains(0);
+ for (int column = 1; column < actualColumnCount; column++) {
+ boolean used = usedColumns.contains(column);
+ if (used || prevUsed != used) {
+ newColumn++;
+ prevUsed = used;
+ }
+ columnMap[column] = newColumn;
+ }
+ newColumn++;
+ columnMap[actualColumnCount] = newColumn;
+ assert columnMap[0] == 0;
+
+ int[] rowMap = new int[actualRowCount + 1]; // +1: Easily handle rowSpans as well
+ int newRow = 0;
+ prevUsed = usedRows.contains(0);
+ for (int row = 1; row < actualRowCount; row++) {
+ boolean used = usedRows.contains(row);
+ if (used || prevUsed != used) {
+ newRow++;
+ prevUsed = used;
+ }
+ rowMap[row] = newRow;
+ }
+ newRow++;
+ rowMap[actualRowCount] = newRow;
+ assert rowMap[0] == 0;
+
+
+ // Adjust column and row numbers to account for deletions: for a given cell, if it
+ // is to the right of a deleted column, reduce its column number, and if it only
+ // spans across the deleted column, reduce its column span.
+ for (ViewData view : mChildViews) {
+ if (removedViews.contains(view)) {
+ continue;
+ }
+ int newColumnStart = columnMap[Math.min(columnMap.length - 1, view.column)];
+ // Gracefully handle rogue/invalid columnSpans in the XML
+ int newColumnEnd = columnMap[Math.min(columnMap.length - 1,
+ view.column + view.columnSpan)];
+ if (newColumnStart != view.column) {
+ view.column = newColumnStart;
+ setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column);
+ }
+
+ int columnSpan = newColumnEnd - newColumnStart;
+ if (columnSpan != view.columnSpan) {
+ if (columnSpan >= 1) {
+ view.columnSpan = columnSpan;
+ setColumnSpanAttribute(view.node, view.columnSpan);
+ } // else: merging spacing columns together
+ }
+
+
+ int newRowStart = rowMap[Math.min(rowMap.length - 1, view.row)];
+ int newRowEnd = rowMap[Math.min(rowMap.length - 1, view.row + view.rowSpan)];
+ if (newRowStart != view.row) {
+ view.row = newRowStart;
+ setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row);
+ }
+
+ int rowSpan = newRowEnd - newRowStart;
+ if (rowSpan != view.rowSpan) {
+ if (rowSpan >= 1) {
+ view.rowSpan = rowSpan;
+ setRowSpanAttribute(view.node, view.rowSpan);
+ } // else: merging spacing rows together
+ }
+ }
+
+ // Merge spacers (and add spacers for newly empty columns)
+ int start = 0;
+ while (start < actualColumnCount) {
+ // Find next unused span
+ while (start < actualColumnCount && usedColumns.contains(start)) {
+ start++;
+ }
+ if (start == actualColumnCount) {
+ break;
+ }
+ assert !usedColumns.contains(start);
+ // Find the next span of unused columns and produce a SINGLE
+ // spacer for that range (unless it's a zero-sized columns)
+ int end = start + 1;
+ for (; end < actualColumnCount; end++) {
+ if (usedColumns.contains(end)) {
+ break;
+ }
+ }
+
+ // Add up column sizes
+ int width = getColumnWidth(start, end - start);
+
+ // Find all spacers: the first one found should be moved to the start column
+ // and assigned to the full height of the columns, and
+ // the column count reduced by the corresponding amount
+
+ // TODO: if width = 0, fully remove
+
+ boolean isFirstSpacer = true;
+ for (int column = start; column < end; column++) {
+ Collection<ViewData> spacers = columnSpacers.get(column);
+ if (spacers != null && !spacers.isEmpty()) {
+ // Avoid ConcurrentModificationException since we're inserting into the
+ // map within this loop (always at a different index, but the map doesn't
+ // know that)
+ spacers = new ArrayList<ViewData>(spacers);
+ for (ViewData spacer : spacers) {
+ if (isFirstSpacer) {
+ isFirstSpacer = false;
+ spacer.column = columnMap[start];
+ setGridAttribute(spacer.node, ATTR_LAYOUT_COLUMN, spacer.column);
+ if (end - start > 1) {
+ // Compute a merged width for all the spacers (not needed if
+ // there's just one spacer; it should already have the correct width)
+ int columnWidthDp = mRulesEngine.pxToDp(width);
+ spacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH,
+ String.format(VALUE_N_DP, columnWidthDp));
+ }
+ columnSpacers.put(start, spacer);
+ } else {
+ removedViews.add(spacer); // Mark for model removal
+ layout.removeChild(spacer.node);
+ }
+ }
+ }
+ }
+
+ if (isFirstSpacer) {
+ // No spacer: create one
+ int columnWidthDp = mRulesEngine.pxToDp(width);
+ addSpacer(layout, -1, UNDEFINED, columnMap[start], columnWidthDp, DEFAULT_CELL_HEIGHT);
+ }
+
+ start = end;
+ }
+ actualColumnCount = newColumn;
+//if (usedColumns.contains(newColumn)) {
+// // TODO: This may be totally wrong for right aligned content!
+// actualColumnCount++;
+//}
+
+ // Merge spacers for rows
+ start = 0;
+ while (start < actualRowCount) {
+ // Find next unused span
+ while (start < actualRowCount && usedRows.contains(start)) {
+ start++;
+ }
+ if (start == actualRowCount) {
+ break;
+ }
+ assert !usedRows.contains(start);
+ // Find the next span of unused rows and produce a SINGLE
+ // spacer for that range (unless it's a zero-sized rows)
+ int end = start + 1;
+ for (; end < actualRowCount; end++) {
+ if (usedRows.contains(end)) {
+ break;
+ }
+ }
+
+ // Add up row sizes
+ int height = getRowHeight(start, end - start);
+
+ // Find all spacers: the first one found should be moved to the start row
+ // and assigned to the full height of the rows, and
+ // the row count reduced by the corresponding amount
+
+ // TODO: if width = 0, fully remove
+
+ boolean isFirstSpacer = true;
+ for (int row = start; row < end; row++) {
+ Collection<ViewData> spacers = rowSpacers.get(row);
+ if (spacers != null && !spacers.isEmpty()) {
+ // Avoid ConcurrentModificationException since we're inserting into the
+ // map within this loop (always at a different index, but the map doesn't
+ // know that)
+ spacers = new ArrayList<ViewData>(spacers);
+ for (ViewData spacer : spacers) {
+ if (isFirstSpacer) {
+ isFirstSpacer = false;
+ spacer.row = rowMap[start];
+ setGridAttribute(spacer.node, ATTR_LAYOUT_ROW, spacer.row);
+ if (end - start > 1) {
+ // Compute a merged width for all the spacers (not needed if
+ // there's just one spacer; it should already have the correct height)
+ int rowHeightDp = mRulesEngine.pxToDp(height);
+ spacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT,
+ String.format(VALUE_N_DP, rowHeightDp));
+ }
+ rowSpacers.put(start, spacer);
+ } else {
+ removedViews.add(spacer); // Mark for model removal
+ layout.removeChild(spacer.node);
+ }
+ }
+ }
+ }
+
+ if (isFirstSpacer) {
+ // No spacer: create one
+ int rowWidthDp = mRulesEngine.pxToDp(height);
+ addSpacer(layout, -1, rowMap[start], UNDEFINED, DEFAULT_CELL_WIDTH, rowWidthDp);
+ }
+
+ start = end;
+ }
+ actualRowCount = newRow;
+// if (usedRows.contains(newRow)) {
+// actualRowCount++;
+// }
+
+ // Update the model: remove removed children from the view data list
+ if (removedViews.size() <= 2) {
+ mChildViews.removeAll(removedViews);
+ } else {
+ List<ViewData> remaining =
+ new ArrayList<ViewData>(mChildViews.size() - removedViews.size());
+ for (ViewData view : mChildViews) {
+ if (!removedViews.contains(view)) {
+ remaining.add(view);
+ }
+ }
+ mChildViews = remaining;
+ }
+
+ // Update the final column and row declared attributes
+ if (declaredColumnCount != UNDEFINED) {
+ declaredColumnCount = actualColumnCount;
+ setGridAttribute(layout, ATTR_COLUMN_COUNT, actualColumnCount);
+ }
+ if (declaredRowCount != UNDEFINED) {
+ declaredRowCount = actualRowCount;
+ setGridAttribute(layout, ATTR_ROW_COUNT, actualRowCount);
+ }
+ }
+
+ /**
+ * Adds a spacer to the given parent, at the given index.
+ *
+ * @param parent the GridLayout
+ * @param index the index to insert the spacer at, or -1 to append
+ * @param row the row to add the spacer to (or {@link #UNDEFINED} to not set a row yet
+ * @param column the column to add the spacer to (or {@link #UNDEFINED} to not set a
+ * column yet
+ * @param widthDp the width in device independent pixels to assign to the spacer
+ * @param heightDp the height in device independent pixels to assign to the spacer
+ * @return the newly added spacer
+ */
+ ViewData addSpacer(INode parent, int index, int row, int column,
+ int widthDp, int heightDp) {
+ INode spacer;
+
+ String tag = FQCN_SPACE;
+ String gridLayout = parent.getFqcn();
+ if (!gridLayout.equals(GRID_LAYOUT) && gridLayout.length() > GRID_LAYOUT.length()) {
+ String pkg = gridLayout.substring(0, gridLayout.length() - GRID_LAYOUT.length());
+ tag = pkg + SPACE;
+ }
+ if (index != -1) {
+ spacer = parent.insertChildAt(tag, index);
+ } else {
+ spacer = parent.appendChild(tag);
+ }
+
+ ViewData view = new ViewData(spacer, index != -1 ? index : mChildViews.size());
+ mChildViews.add(view);
+
+ if (row != UNDEFINED) {
+ view.row = row;
+ setGridAttribute(spacer, ATTR_LAYOUT_ROW, row);
+ }
+ if (column != UNDEFINED) {
+ view.column = column;
+ setGridAttribute(spacer, ATTR_LAYOUT_COLUMN, column);
+ }
+ if (widthDp > 0) {
+ spacer.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH,
+ String.format(VALUE_N_DP, widthDp));
+ }
+ if (heightDp > 0) {
+ spacer.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT,
+ String.format(VALUE_N_DP, heightDp));
+ }
+
+ // Temporary hack
+ if (GridLayoutRule.sDebugGridLayout) {
+ //String id = NEW_ID_PREFIX + "s";
+ //if (row == 0) {
+ // id += "c";
+ //}
+ //if (column == 0) {
+ // id += "r";
+ //}
+ //if (row > 0) {
+ // id += Integer.toString(row);
+ //}
+ //if (column > 0) {
+ // id += Integer.toString(column);
+ //}
+ String id = NEW_ID_PREFIX + "spacer_" //$NON-NLS-1$
+ + Integer.toString(System.identityHashCode(spacer)).substring(0, 3);
+ spacer.setAttribute(ANDROID_URI, ATTR_ID, id);
+ }
+
+
+ return view;
+ }
+
+ /**
+ * Returns the string value of the given attribute, or null if it does not
+ * exist. This only works for attributes that are GridLayout specific, such
+ * as columnCount, layout_column, layout_row_span, etc.
+ *
+ * @param node the target node
+ * @param name the attribute name (which must be in the android: namespace)
+ * @return the attribute value or null
+ */
+
+ public String getGridAttribute(INode node, String name) {
+ return node.getStringAttr(getNamespace(), name);
+ }
+
+ /**
+ * Returns the integer value of the given attribute, or the given defaultValue if the
+ * attribute was not set. This only works for attributes that are GridLayout specific,
+ * such as columnCount, layout_column, layout_row_span, etc.
+ *
+ * @param node the target node
+ * @param attribute the attribute name (which must be in the android: namespace)
+ * @param defaultValue the default value to use if the value is not set
+ * @return the attribute integer value
+ */
+ private int getGridAttribute(INode node, String attribute, int defaultValue) {
+ String valueString = node.getStringAttr(getNamespace(), attribute);
+ if (valueString != null) {
+ try {
+ return Integer.decode(valueString);
+ } catch (NumberFormatException nufe) {
+ // Ignore - error in user's XML
+ }
+ }
+
+ return defaultValue;
+ }
+
+ /**
+ * Returns the number of children views in the GridLayout
+ *
+ * @return the number of children views in the GridLayout
+ */
+ public int getViewCount() {
+ return mChildViews.size();
+ }
+
+ /**
+ * Returns true if the given class name represents a spacer
+ *
+ * @param fqcn the fully qualified class name
+ * @return true if this is a spacer
+ */
+ public static boolean isSpace(String fqcn) {
+ return FQCN_SPACE.equals(fqcn) || FQCN_SPACE_V7.equals(fqcn);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/gridmode.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/gridmode.png
new file mode 100644
index 000000000..59e0a4511
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/gridmode.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/hlinear.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/hlinear.png
new file mode 100644
index 000000000..b293fe7c5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/hlinear.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/margins.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/margins.png
new file mode 100644
index 000000000..b0d814116
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/margins.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 000000000..447d2d880
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ConstraintPainter.java
@@ -0,0 +1,783 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.common.layout.relative;
+
+import static com.android.ide.common.api.DrawingStyle.DEPENDENCY;
+import static com.android.ide.common.api.DrawingStyle.GUIDELINE;
+import static com.android.ide.common.api.DrawingStyle.GUIDELINE_DASHED;
+import static com.android.ide.common.api.SegmentType.BASELINE;
+import static com.android.ide.common.api.SegmentType.BOTTOM;
+import static com.android.ide.common.api.SegmentType.CENTER_HORIZONTAL;
+import static com.android.ide.common.api.SegmentType.CENTER_VERTICAL;
+import static com.android.ide.common.api.SegmentType.LEFT;
+import static com.android.ide.common.api.SegmentType.RIGHT;
+import static com.android.ide.common.api.SegmentType.TOP;
+import static com.android.ide.common.api.SegmentType.UNKNOWN;
+import static com.android.ide.common.layout.relative.ConstraintType.ALIGN_BASELINE;
+import static com.android.ide.common.layout.relative.ConstraintType.ALIGN_BOTTOM;
+import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_ABOVE;
+import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_BELOW;
+import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_LEFT_OF;
+import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_RIGHT_OF;
+
+import com.android.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.IGraphics;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.common.layout.relative.DependencyGraph.Constraint;
+import com.android.ide.common.layout.relative.DependencyGraph.ViewData;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The {@link ConstraintPainter} is responsible for painting relative layout constraints -
+ * such as a source node having its top edge constrained to a target node with a given margin.
+ * This painter is used both to show static constraints, as well as visualizing proposed
+ * constraints during a move or resize operation.
+ */
+public class ConstraintPainter {
+ /** The size of the arrow head */
+ private static final int ARROW_SIZE = 5;
+ /** Size (height for horizontal, and width for vertical) parent feedback rectangles */
+ private static final int PARENT_RECT_SIZE = 12;
+
+ /**
+ * Paints a given match as a constraint.
+ *
+ * @param graphics the graphics context
+ * @param sourceBounds the source bounds
+ * @param match the match
+ */
+ static void paintConstraint(IGraphics graphics, Rect sourceBounds, Match match) {
+ Rect targetBounds = match.edge.node.getBounds();
+ ConstraintType type = match.type;
+ assert type != null;
+ paintConstraint(graphics, type, match.with.node, sourceBounds, match.edge.node,
+ targetBounds, null /* allConstraints */, true /* highlightTargetEdge */);
+ }
+
+ /**
+ * Paints a constraint.
+ * <p>
+ * TODO: when there are multiple links originating in the same direction from
+ * center, maybe offset them slightly from each other?
+ *
+ * @param graphics the graphics context to draw into
+ * @param constraint The constraint to be drawn
+ */
+ private static void paintConstraint(IGraphics graphics, Constraint constraint,
+ Set<Constraint> allConstraints) {
+ ViewData source = constraint.from;
+ ViewData target = constraint.to;
+
+ INode sourceNode = source.node;
+ INode targetNode = target.node;
+ if (sourceNode == targetNode) {
+ // Self reference - don't visualize
+ return;
+ }
+
+ Rect sourceBounds = sourceNode.getBounds();
+ Rect targetBounds = targetNode.getBounds();
+ paintConstraint(graphics, constraint.type, sourceNode, sourceBounds, targetNode,
+ targetBounds, allConstraints, false /* highlightTargetEdge */);
+ }
+
+ /**
+ * Paint selection feedback by painting constraints for the selected nodes
+ *
+ * @param graphics the graphics context
+ * @param parentNode the parent relative layout
+ * @param childNodes the nodes whose constraints should be painted
+ * @param showDependents whether incoming constraints should be shown as well
+ */
+ public static void paintSelectionFeedback(IGraphics graphics, INode parentNode,
+ List<? extends INode> childNodes, boolean showDependents) {
+
+ DependencyGraph dependencyGraph = new DependencyGraph(parentNode);
+ Set<INode> horizontalDeps = dependencyGraph.dependsOn(childNodes, false /* vertical */);
+ Set<INode> verticalDeps = dependencyGraph.dependsOn(childNodes, true /* vertical */);
+ Set<INode> deps = new HashSet<INode>(horizontalDeps.size() + verticalDeps.size());
+ deps.addAll(horizontalDeps);
+ deps.addAll(verticalDeps);
+ if (deps.size() > 0) {
+ graphics.useStyle(DEPENDENCY);
+ for (INode node : deps) {
+ // Don't highlight the selected nodes themselves
+ if (childNodes.contains(node)) {
+ continue;
+ }
+ Rect bounds = node.getBounds();
+ graphics.fillRect(bounds);
+ }
+ }
+
+ graphics.useStyle(GUIDELINE);
+ for (INode childNode : childNodes) {
+ ViewData view = dependencyGraph.getView(childNode);
+ if (view == null) {
+ continue;
+ }
+
+ // Paint all incoming constraints
+ if (showDependents) {
+ paintConstraints(graphics, view.dependedOnBy);
+ }
+
+ // Paint all outgoing constraints
+ paintConstraints(graphics, view.dependsOn);
+ }
+ }
+
+ /**
+ * Paints a set of constraints.
+ */
+ private static void paintConstraints(IGraphics graphics, List<Constraint> constraints) {
+ Set<Constraint> mutableConstraintSet = new HashSet<Constraint>(constraints);
+
+ // WORKAROUND! Hide alignBottom attachments if we also have a alignBaseline
+ // constraint; this is because we also *add* alignBottom attachments when you add
+ // alignBaseline constraints to work around a surprising behavior of baseline
+ // constraints.
+ for (Constraint constraint : constraints) {
+ if (constraint.type == ALIGN_BASELINE) {
+ // Remove any baseline
+ for (Constraint c : constraints) {
+ if (c.type == ALIGN_BOTTOM && c.to.node == constraint.to.node) {
+ mutableConstraintSet.remove(c);
+ }
+ }
+ }
+ }
+
+ for (Constraint constraint : constraints) {
+ // paintConstraint can digest more than one constraint, so we need to keep
+ // checking to see if the given constraint is still relevant.
+ if (mutableConstraintSet.contains(constraint)) {
+ paintConstraint(graphics, constraint, mutableConstraintSet);
+ }
+ }
+ }
+
+ /**
+ * Paints a constraint of the given type from the given source node, to the
+ * given target node, with the specified bounds.
+ */
+ private static void paintConstraint(IGraphics graphics, ConstraintType type, INode sourceNode,
+ Rect sourceBounds, INode targetNode, Rect targetBounds,
+ Set<Constraint> allConstraints, boolean highlightTargetEdge) {
+
+ SegmentType sourceSegmentTypeX = type.sourceSegmentTypeX;
+ SegmentType sourceSegmentTypeY = type.sourceSegmentTypeY;
+ SegmentType targetSegmentTypeX = type.targetSegmentTypeX;
+ SegmentType targetSegmentTypeY = type.targetSegmentTypeY;
+
+ // Horizontal center constraint?
+ if (sourceSegmentTypeX == CENTER_VERTICAL && targetSegmentTypeX == CENTER_VERTICAL) {
+ paintHorizontalCenterConstraint(graphics, sourceBounds, targetBounds);
+ return;
+ }
+
+ // Vertical center constraint?
+ if (sourceSegmentTypeY == CENTER_HORIZONTAL && targetSegmentTypeY == CENTER_HORIZONTAL) {
+ paintVerticalCenterConstraint(graphics, sourceBounds, targetBounds);
+ return;
+ }
+
+ // Corner constraint?
+ if (allConstraints != null
+ && (type == LAYOUT_ABOVE || type == LAYOUT_BELOW
+ || type == LAYOUT_LEFT_OF || type == LAYOUT_RIGHT_OF)) {
+ if (paintCornerConstraint(graphics, type, sourceNode, sourceBounds, targetNode,
+ targetBounds, allConstraints)) {
+ return;
+ }
+ }
+
+ // Vertical constraint?
+ if (sourceSegmentTypeX == UNKNOWN) {
+ paintVerticalConstraint(graphics, type, sourceNode, sourceBounds, targetNode,
+ targetBounds, highlightTargetEdge);
+ return;
+ }
+
+ // Horizontal constraint?
+ if (sourceSegmentTypeY == UNKNOWN) {
+ paintHorizontalConstraint(graphics, type, sourceNode, sourceBounds, targetNode,
+ targetBounds, highlightTargetEdge);
+ return;
+ }
+
+ // This shouldn't happen - it means we have a constraint that defines all sides
+ // and is not a centering constraint
+ assert false;
+ }
+
+ /**
+ * Paints a corner constraint, or returns false if this constraint is not a corner.
+ * A corner is one where there are two constraints from this source node to the
+ * same target node, one horizontal and one vertical, to the closest edges on
+ * the target node.
+ * <p>
+ * Corners are a common occurrence. If we treat the horizontal and vertical
+ * constraints separately (below & toRightOf), then we end up with a lot of
+ * extra lines and arrows -- e.g. two shared edges and arrows pointing to these
+ * shared edges:
+ *
+ * <pre>
+ * +--------+ |
+ * | Target -->
+ * +----|---+ |
+ * v
+ * - - - - - -|- - - - - -
+ * ^
+ * | +---|----+
+ * <-- Source |
+ * | +--------+
+ *
+ * Instead, we can simply draw a diagonal arrow here to represent BOTH constraints and
+ * reduce clutter:
+ *
+ * +---------+
+ * | Target _|
+ * +-------|\+
+ * \
+ * \--------+
+ * | Source |
+ * +--------+
+ * </pre>
+ *
+ * @param graphics the graphics context to draw
+ * @param type the constraint to be drawn
+ * @param sourceNode the source node
+ * @param sourceBounds the bounds of the source node
+ * @param targetNode the target node
+ * @param targetBounds the bounds of the target node
+ * @param allConstraints the set of all constraints; if a corner is found and painted the
+ * matching corner constraint is removed from the set
+ * @return true if the constraint was handled and painted as a corner, false otherwise
+ */
+ private static boolean paintCornerConstraint(IGraphics graphics, ConstraintType type,
+ INode sourceNode, Rect sourceBounds, INode targetNode, Rect targetBounds,
+ Set<Constraint> allConstraints) {
+
+ SegmentType sourceSegmentTypeX = type.sourceSegmentTypeX;
+ SegmentType sourceSegmentTypeY = type.sourceSegmentTypeY;
+ SegmentType targetSegmentTypeX = type.targetSegmentTypeX;
+ SegmentType targetSegmentTypeY = type.targetSegmentTypeY;
+
+ ConstraintType opposite1 = null, opposite2 = null;
+ switch (type) {
+ case LAYOUT_BELOW:
+ case LAYOUT_ABOVE:
+ opposite1 = LAYOUT_LEFT_OF;
+ opposite2 = LAYOUT_RIGHT_OF;
+ break;
+ case LAYOUT_LEFT_OF:
+ case LAYOUT_RIGHT_OF:
+ opposite1 = LAYOUT_ABOVE;
+ opposite2 = LAYOUT_BELOW;
+ break;
+ default:
+ return false;
+ }
+ Constraint pair = null;
+ for (Constraint constraint : allConstraints) {
+ if ((constraint.type == opposite1 || constraint.type == opposite2) &&
+ constraint.to.node == targetNode && constraint.from.node == sourceNode) {
+ pair = constraint;
+ break;
+ }
+ }
+
+ // TODO -- ensure that the nodes are adjacent! In other words, that
+ // their bounds are within N pixels.
+
+ if (pair != null) {
+ // Visualize the corner constraint
+ if (sourceSegmentTypeX == UNKNOWN) {
+ sourceSegmentTypeX = pair.type.sourceSegmentTypeX;
+ }
+ if (sourceSegmentTypeY == UNKNOWN) {
+ sourceSegmentTypeY = pair.type.sourceSegmentTypeY;
+ }
+ if (targetSegmentTypeX == UNKNOWN) {
+ targetSegmentTypeX = pair.type.targetSegmentTypeX;
+ }
+ if (targetSegmentTypeY == UNKNOWN) {
+ targetSegmentTypeY = pair.type.targetSegmentTypeY;
+ }
+
+ int x1, y1, x2, y2;
+ if (sourceSegmentTypeX == LEFT) {
+ x1 = sourceBounds.x + 1 * sourceBounds.w / 4;
+ } else {
+ x1 = sourceBounds.x + 3 * sourceBounds.w / 4;
+ }
+ if (sourceSegmentTypeY == TOP) {
+ y1 = sourceBounds.y + 1 * sourceBounds.h / 4;
+ } else {
+ y1 = sourceBounds.y + 3 * sourceBounds.h / 4;
+ }
+ if (targetSegmentTypeX == LEFT) {
+ x2 = targetBounds.x + 1 * targetBounds.w / 4;
+ } else {
+ x2 = targetBounds.x + 3 * targetBounds.w / 4;
+ }
+ if (targetSegmentTypeY == TOP) {
+ y2 = targetBounds.y + 1 * targetBounds.h / 4;
+ } else {
+ y2 = targetBounds.y + 3 * targetBounds.h / 4;
+ }
+
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(x1, y1, x2, y2, ARROW_SIZE);
+
+ // Don't process this constraint on its own later.
+ allConstraints.remove(pair);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Paints a vertical constraint, handling the various scenarios where there are
+ * margins, or where the two nodes overlap horizontally and where they don't, etc.
+ * <p>
+ * Here's an example of what will be shown for a "below" constraint where the
+ * nodes do not overlap horizontally and the target node has a bottom margin:
+ * <pre>
+ * +--------+
+ * | Target |
+ * +--------+
+ * |
+ * v
+ * - - - - - - - - - - - - - -
+ * ^
+ * |
+ * +--------+
+ * | Source |
+ * +--------+
+ * </pre>
+ */
+ private static void paintVerticalConstraint(IGraphics graphics, ConstraintType type,
+ INode sourceNode, Rect sourceBounds, INode targetNode, Rect targetBounds,
+ boolean highlightTargetEdge) {
+ SegmentType sourceSegmentTypeY = type.sourceSegmentTypeY;
+ SegmentType targetSegmentTypeY = type.targetSegmentTypeY;
+ Margins targetMargins = targetNode.getMargins();
+
+ assert sourceSegmentTypeY != UNKNOWN;
+ assert targetBounds != null;
+
+ int sourceY = sourceSegmentTypeY.getY(sourceNode, sourceBounds);
+ int targetY = targetSegmentTypeY ==
+ UNKNOWN ? sourceY : targetSegmentTypeY.getY(targetNode, targetBounds);
+
+ if (highlightTargetEdge && type.isRelativeToParentEdge()) {
+ graphics.useStyle(DrawingStyle.DROP_ZONE_ACTIVE);
+ graphics.fillRect(targetBounds.x, targetY - PARENT_RECT_SIZE / 2,
+ targetBounds.x2(), targetY + PARENT_RECT_SIZE / 2);
+ }
+
+ // First see if the two views overlap horizontally. If so, we can just draw a direct
+ // arrow from the source up to (or down to) the target.
+ //
+ // +--------+
+ // | Target |
+ // +--------+
+ // ^
+ // |
+ // |
+ // +--------+
+ // | Source |
+ // +--------+
+ //
+ int maxLeft = Math.max(sourceBounds.x, targetBounds.x);
+ int minRight = Math.min(sourceBounds.x2(), targetBounds.x2());
+
+ int center = (maxLeft + minRight) / 2;
+ if (center > sourceBounds.x && center < sourceBounds.x2()) {
+ // Yes, the lines overlap -- just draw a straight arrow
+ //
+ //
+ // If however there is a margin on the target edge, it should be drawn like this:
+ //
+ // +--------+
+ // | Target |
+ // +--------+
+ // |
+ // |
+ // v
+ // - - - - - - -
+ // ^
+ // |
+ // |
+ // +--------+
+ // | Source |
+ // +--------+
+ //
+ // Use a minimum threshold for this visualization since it doesn't look good
+ // for small margins
+ if (targetSegmentTypeY == BOTTOM && targetMargins.bottom > 5) {
+ int sharedY = targetY + targetMargins.bottom;
+ if (sourceY > sharedY + 2) { // Skip when source falls on the margin line
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(targetBounds.x, sharedY, targetBounds.x2(), sharedY);
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(center, sourceY, center, sharedY + 2, ARROW_SIZE);
+ graphics.drawArrow(center, targetY, center, sharedY - 3, ARROW_SIZE);
+ } else {
+ graphics.useStyle(GUIDELINE);
+ // Draw reverse arrow to make it clear the node is as close
+ // at it can be
+ graphics.drawArrow(center, targetY, center, sourceY, ARROW_SIZE);
+ }
+ return;
+ } else if (targetSegmentTypeY == TOP && targetMargins.top > 5) {
+ int sharedY = targetY - targetMargins.top;
+ if (sourceY < sharedY - 2) {
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(targetBounds.x, sharedY, targetBounds.x2(), sharedY);
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(center, sourceY, center, sharedY - 3, ARROW_SIZE);
+ graphics.drawArrow(center, targetY, center, sharedY + 3, ARROW_SIZE);
+ } else {
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(center, targetY, center, sourceY, ARROW_SIZE);
+ }
+ return;
+ }
+
+ // TODO: If the center falls smack in the center of the sourceBounds,
+ // AND the source node is part of the selection, then adjust the
+ // center location such that it is off to the side, let's say 1/4 or 3/4 of
+ // the overlap region, to ensure that it does not overlap the center selection
+ // handle
+
+ // When the constraint is for two immediately adjacent edges, we
+ // need to make some adjustments to make sure the arrow points in the right
+ // direction
+ if (sourceY == targetY) {
+ if (sourceSegmentTypeY == BOTTOM || sourceSegmentTypeY == BASELINE) {
+ sourceY -= 2 * ARROW_SIZE;
+ } else if (sourceSegmentTypeY == TOP) {
+ sourceY += 2 * ARROW_SIZE;
+ } else {
+ assert sourceSegmentTypeY == CENTER_HORIZONTAL : sourceSegmentTypeY;
+ sourceY += sourceBounds.h / 2 - 2 * ARROW_SIZE;
+ }
+ } else if (sourceSegmentTypeY == BASELINE) {
+ sourceY = targetY - 2 * ARROW_SIZE;
+ }
+
+ // Center the vertical line in the overlap region
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(center, sourceY, center, targetY, ARROW_SIZE);
+
+ return;
+ }
+
+ // If there is no horizontal overlap in the vertical constraints, then we
+ // will show the attachment relative to a dashed line that extends beyond
+ // the target bounds, like this:
+ //
+ // +--------+
+ // | Target |
+ // +--------+ - - - - - - - - -
+ // ^
+ // |
+ // +--------+
+ // | Source |
+ // +--------+
+ //
+ // However, if the target node has a vertical margin, we may need to offset
+ // the line:
+ //
+ // +--------+
+ // | Target |
+ // +--------+
+ // |
+ // v
+ // - - - - - - - - - - - - - -
+ // ^
+ // |
+ // +--------+
+ // | Source |
+ // +--------+
+ //
+ // If not, we'll need to indicate a shared edge. This is the edge that separate
+ // them (but this will require me to evaluate margins!)
+
+ // Compute overlap region and pick the middle
+ int sharedY = targetSegmentTypeY ==
+ UNKNOWN ? sourceY : targetSegmentTypeY.getY(targetNode, targetBounds);
+ if (type.relativeToMargin) {
+ if (targetSegmentTypeY == TOP) {
+ sharedY -= targetMargins.top;
+ } else if (targetSegmentTypeY == BOTTOM) {
+ sharedY += targetMargins.bottom;
+ }
+ }
+
+ int startX;
+ int endX;
+ if (center <= sourceBounds.x) {
+ startX = targetBounds.x + targetBounds.w / 4;
+ endX = sourceBounds.x2();
+ } else {
+ assert (center >= sourceBounds.x2());
+ startX = sourceBounds.x;
+ endX = targetBounds.x + 3 * targetBounds.w / 4;
+ }
+ // Must draw segmented line instead
+ // Place the arrow 1/4 instead of 1/2 in the source to avoid overlapping with the
+ // selection handles
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(startX, sharedY, endX, sharedY);
+
+ // Adjust position of source arrow such that it does not sit across edge; it
+ // should point directly at the edge
+ if (Math.abs(sharedY - sourceY) < 2 * ARROW_SIZE) {
+ if (sourceSegmentTypeY == BASELINE) {
+ sourceY = sharedY - 2 * ARROW_SIZE;
+ } else if (sourceSegmentTypeY == TOP) {
+ sharedY = sourceY;
+ sourceY = sharedY + 2 * ARROW_SIZE;
+ } else {
+ sharedY = sourceY;
+ sourceY = sharedY - 2 * ARROW_SIZE;
+ }
+ }
+
+ graphics.useStyle(GUIDELINE);
+
+ // Draw the line from the source anchor to the shared edge
+ int x = sourceBounds.x + ((sourceSegmentTypeY == BASELINE) ?
+ sourceBounds.w / 2 : sourceBounds.w / 4);
+ graphics.drawArrow(x, sourceY, x, sharedY, ARROW_SIZE);
+
+ // Draw the line from the target to the horizontal shared edge
+ int tx = targetBounds.centerX();
+ if (targetSegmentTypeY == TOP) {
+ int ty = targetBounds.y;
+ int margin = targetMargins.top;
+ if (margin == 0 || !type.relativeToMargin) {
+ graphics.drawArrow(tx, ty + 2 * ARROW_SIZE, tx, ty, ARROW_SIZE);
+ } else {
+ graphics.drawArrow(tx, ty, tx, ty - margin, ARROW_SIZE);
+ }
+ } else if (targetSegmentTypeY == BOTTOM) {
+ int ty = targetBounds.y2();
+ int margin = targetMargins.bottom;
+ if (margin == 0 || !type.relativeToMargin) {
+ graphics.drawArrow(tx, ty - 2 * ARROW_SIZE, tx, ty, ARROW_SIZE);
+ } else {
+ graphics.drawArrow(tx, ty, tx, ty + margin, ARROW_SIZE);
+ }
+ } else {
+ assert targetSegmentTypeY == BASELINE : targetSegmentTypeY;
+ int ty = targetSegmentTypeY.getY(targetNode, targetBounds);
+ graphics.drawArrow(tx, ty - 2 * ARROW_SIZE, tx, ty, ARROW_SIZE);
+ }
+
+ return;
+ }
+
+ /**
+ * Paints a horizontal constraint, handling the various scenarios where there are margins,
+ * or where the two nodes overlap horizontally and where they don't, etc.
+ */
+ private static void paintHorizontalConstraint(IGraphics graphics, ConstraintType type,
+ INode sourceNode, Rect sourceBounds, INode targetNode, Rect targetBounds,
+ boolean highlightTargetEdge) {
+ SegmentType sourceSegmentTypeX = type.sourceSegmentTypeX;
+ SegmentType targetSegmentTypeX = type.targetSegmentTypeX;
+ Margins targetMargins = targetNode.getMargins();
+
+ assert sourceSegmentTypeX != UNKNOWN;
+ assert targetBounds != null;
+
+ // See paintVerticalConstraint for explanations of the various cases.
+
+ int sourceX = sourceSegmentTypeX.getX(sourceNode, sourceBounds);
+ int targetX = targetSegmentTypeX == UNKNOWN ?
+ sourceX : targetSegmentTypeX.getX(targetNode, targetBounds);
+
+ if (highlightTargetEdge && type.isRelativeToParentEdge()) {
+ graphics.useStyle(DrawingStyle.DROP_ZONE_ACTIVE);
+ graphics.fillRect(targetX - PARENT_RECT_SIZE / 2, targetBounds.y,
+ targetX + PARENT_RECT_SIZE / 2, targetBounds.y2());
+ }
+
+ int maxTop = Math.max(sourceBounds.y, targetBounds.y);
+ int minBottom = Math.min(sourceBounds.y2(), targetBounds.y2());
+
+ // First see if the two views overlap vertically. If so, we can just draw a direct
+ // arrow from the source over to the target.
+ int center = (maxTop + minBottom) / 2;
+ if (center > sourceBounds.y && center < sourceBounds.y2()) {
+ // See if we should draw a margin line
+ if (targetSegmentTypeX == RIGHT && targetMargins.right > 5) {
+ int sharedX = targetX + targetMargins.right;
+ if (sourceX > sharedX + 2) { // Skip when source falls on the margin line
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(sharedX, targetBounds.y, sharedX, targetBounds.y2());
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(sourceX, center, sharedX + 2, center, ARROW_SIZE);
+ graphics.drawArrow(targetX, center, sharedX - 3, center, ARROW_SIZE);
+ } else {
+ graphics.useStyle(GUIDELINE);
+ // Draw reverse arrow to make it clear the node is as close
+ // at it can be
+ graphics.drawArrow(targetX, center, sourceX, center, ARROW_SIZE);
+ }
+ return;
+ } else if (targetSegmentTypeX == LEFT && targetMargins.left > 5) {
+ int sharedX = targetX - targetMargins.left;
+ if (sourceX < sharedX - 2) {
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(sharedX, targetBounds.y, sharedX, targetBounds.y2());
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(sourceX, center, sharedX - 3, center, ARROW_SIZE);
+ graphics.drawArrow(targetX, center, sharedX + 3, center, ARROW_SIZE);
+ } else {
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(targetX, center, sourceX, center, ARROW_SIZE);
+ }
+ return;
+ }
+
+ if (sourceX == targetX) {
+ if (sourceSegmentTypeX == RIGHT) {
+ sourceX -= 2 * ARROW_SIZE;
+ } else if (sourceSegmentTypeX == LEFT ) {
+ sourceX += 2 * ARROW_SIZE;
+ } else {
+ assert sourceSegmentTypeX == CENTER_VERTICAL : sourceSegmentTypeX;
+ sourceX += sourceBounds.w / 2 - 2 * ARROW_SIZE;
+ }
+ }
+
+ graphics.useStyle(GUIDELINE);
+ graphics.drawArrow(sourceX, center, targetX, center, ARROW_SIZE);
+ return;
+ }
+
+ // Segment line
+
+ // Compute overlap region and pick the middle
+ int sharedX = targetSegmentTypeX == UNKNOWN ?
+ sourceX : targetSegmentTypeX.getX(targetNode, targetBounds);
+ if (type.relativeToMargin) {
+ if (targetSegmentTypeX == LEFT) {
+ sharedX -= targetMargins.left;
+ } else if (targetSegmentTypeX == RIGHT) {
+ sharedX += targetMargins.right;
+ }
+ }
+
+ int startY, endY;
+ if (center <= sourceBounds.y) {
+ startY = targetBounds.y + targetBounds.h / 4;
+ endY = sourceBounds.y2();
+ } else {
+ assert (center >= sourceBounds.y2());
+ startY = sourceBounds.y;
+ endY = targetBounds.y + 3 * targetBounds.h / 2;
+ }
+
+ // Must draw segmented line instead
+ // Place at 1/4 instead of 1/2 to avoid overlapping with selection handles
+ int y = sourceBounds.y + sourceBounds.h / 4;
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(sharedX, startY, sharedX, endY);
+
+ // Adjust position of source arrow such that it does not sit across edge; it
+ // should point directly at the edge
+ if (Math.abs(sharedX - sourceX) < 2 * ARROW_SIZE) {
+ if (sourceSegmentTypeX == LEFT) {
+ sharedX = sourceX;
+ sourceX = sharedX + 2 * ARROW_SIZE;
+ } else {
+ sharedX = sourceX;
+ sourceX = sharedX - 2 * ARROW_SIZE;
+ }
+ }
+
+ graphics.useStyle(GUIDELINE);
+
+ // Draw the line from the source anchor to the shared edge
+ graphics.drawArrow(sourceX, y, sharedX, y, ARROW_SIZE);
+
+ // Draw the line from the target to the horizontal shared edge
+ int ty = targetBounds.centerY();
+ if (targetSegmentTypeX == LEFT) {
+ int tx = targetBounds.x;
+ int margin = targetMargins.left;
+ if (margin == 0 || !type.relativeToMargin) {
+ graphics.drawArrow(tx + 2 * ARROW_SIZE, ty, tx, ty, ARROW_SIZE);
+ } else {
+ graphics.drawArrow(tx, ty, tx - margin, ty, ARROW_SIZE);
+ }
+ } else {
+ assert targetSegmentTypeX == RIGHT;
+ int tx = targetBounds.x2();
+ int margin = targetMargins.right;
+ if (margin == 0 || !type.relativeToMargin) {
+ graphics.drawArrow(tx - 2 * ARROW_SIZE, ty, tx, ty, ARROW_SIZE);
+ } else {
+ graphics.drawArrow(tx, ty, tx + margin, ty, ARROW_SIZE);
+ }
+ }
+
+ return;
+ }
+
+ /**
+ * Paints a vertical center constraint. The constraint is shown as a dashed line
+ * through the vertical view, and a solid line over the node bounds.
+ */
+ private static void paintVerticalCenterConstraint(IGraphics graphics, Rect sourceBounds,
+ Rect targetBounds) {
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(targetBounds.x, targetBounds.centerY(),
+ targetBounds.x2(), targetBounds.centerY());
+ graphics.useStyle(GUIDELINE);
+ graphics.drawLine(sourceBounds.x, sourceBounds.centerY(),
+ sourceBounds.x2(), sourceBounds.centerY());
+ }
+
+ /**
+ * Paints a horizontal center constraint. The constraint is shown as a dashed line
+ * through the horizontal view, and a solid line over the node bounds.
+ */
+ private static void paintHorizontalCenterConstraint(IGraphics graphics, Rect sourceBounds,
+ Rect targetBounds) {
+ graphics.useStyle(GUIDELINE_DASHED);
+ graphics.drawLine(targetBounds.centerX(), targetBounds.y,
+ targetBounds.centerX(), targetBounds.y2());
+ graphics.useStyle(GUIDELINE);
+ graphics.drawLine(sourceBounds.centerX(), sourceBounds.y,
+ sourceBounds.centerX(), sourceBounds.y2());
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ConstraintType.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ConstraintType.java
new file mode 100644
index 000000000..ed4ac1bf4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ConstraintType.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.common.layout.relative;
+
+import static com.android.ide.common.api.SegmentType.BASELINE;
+import static com.android.ide.common.api.SegmentType.BOTTOM;
+import static com.android.ide.common.api.SegmentType.CENTER_HORIZONTAL;
+import static com.android.ide.common.api.SegmentType.CENTER_VERTICAL;
+import static com.android.ide.common.api.SegmentType.LEFT;
+import static com.android.ide.common.api.SegmentType.RIGHT;
+import static com.android.ide.common.api.SegmentType.TOP;
+import static com.android.ide.common.api.SegmentType.UNKNOWN;
+import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_IN_PARENT;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.SegmentType;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Each constraint type corresponds to a type of constraint available for the
+ * RelativeLayout; for example, {@link #LAYOUT_ABOVE} corresponds to the layout_above constraint.
+ */
+enum ConstraintType {
+ LAYOUT_ABOVE(ATTR_LAYOUT_ABOVE,
+ null /* sourceX */, BOTTOM, null /* targetX */, TOP,
+ false /* targetParent */, true /* horizontalEdge */, false /* verticalEdge */,
+ true /* relativeToMargin */),
+
+ LAYOUT_BELOW(ATTR_LAYOUT_BELOW, null, TOP, null, BOTTOM, false, true, false, true),
+ ALIGN_TOP(ATTR_LAYOUT_ALIGN_TOP, null, TOP, null, TOP, false, true, false, false),
+ ALIGN_BOTTOM(ATTR_LAYOUT_ALIGN_BOTTOM, null, BOTTOM, null, BOTTOM, false, true, false, false),
+ ALIGN_LEFT(ATTR_LAYOUT_ALIGN_LEFT, LEFT, null, LEFT, null, false, false, true, false),
+ ALIGN_RIGHT(ATTR_LAYOUT_ALIGN_RIGHT, RIGHT, null, RIGHT, null, false, false, true, false),
+ LAYOUT_LEFT_OF(ATTR_LAYOUT_TO_LEFT_OF, RIGHT, null, LEFT, null, false, false, true, true),
+ LAYOUT_RIGHT_OF(ATTR_LAYOUT_TO_RIGHT_OF, LEFT, null, RIGHT, null, false, false, true, true),
+ ALIGN_PARENT_TOP(ATTR_LAYOUT_ALIGN_PARENT_TOP, null, TOP, null, TOP, true, true, false, false),
+ ALIGN_BASELINE(ATTR_LAYOUT_ALIGN_BASELINE, null, BASELINE, null, BASELINE, false, true, false,
+ false),
+ ALIGN_PARENT_LEFT(ATTR_LAYOUT_ALIGN_PARENT_LEFT, LEFT, null, LEFT, null, true, false, true,
+ false),
+ ALIGN_PARENT_RIGHT(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, RIGHT, null, RIGHT, null, true, false, true,
+ false),
+ ALIGN_PARENT_BOTTOM(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, null, BOTTOM, null, BOTTOM, true, true,
+ false, false),
+ LAYOUT_CENTER_HORIZONTAL(ATTR_LAYOUT_CENTER_HORIZONTAL, CENTER_VERTICAL, null, CENTER_VERTICAL,
+ null, true, true, false, false),
+ LAYOUT_CENTER_VERTICAL(ATTR_LAYOUT_CENTER_VERTICAL, null, CENTER_HORIZONTAL, null,
+ CENTER_HORIZONTAL, true, false, true, false),
+ LAYOUT_CENTER_IN_PARENT(ATTR_LAYOUT_CENTER_IN_PARENT, CENTER_VERTICAL, CENTER_HORIZONTAL,
+ CENTER_VERTICAL, CENTER_HORIZONTAL, true, true, true, false);
+
+ private ConstraintType(String name, SegmentType sourceSegmentTypeX,
+ SegmentType sourceSegmentTypeY, SegmentType targetSegmentTypeX,
+ SegmentType targetSegmentTypeY, boolean targetParent, boolean horizontalEdge,
+ boolean verticalEdge, boolean relativeToMargin) {
+ assert horizontalEdge || verticalEdge;
+
+ this.name = name;
+ this.sourceSegmentTypeX = sourceSegmentTypeX != null ? sourceSegmentTypeX : UNKNOWN;
+ this.sourceSegmentTypeY = sourceSegmentTypeY != null ? sourceSegmentTypeY : UNKNOWN;
+ this.targetSegmentTypeX = targetSegmentTypeX != null ? targetSegmentTypeX : UNKNOWN;
+ this.targetSegmentTypeY = targetSegmentTypeY != null ? targetSegmentTypeY : UNKNOWN;
+ this.targetParent = targetParent;
+ this.horizontalEdge = horizontalEdge;
+ this.verticalEdge = verticalEdge;
+ this.relativeToMargin = relativeToMargin;
+ }
+
+ /** The attribute name of the constraint */
+ public final String name;
+
+ /** The horizontal position of the source of the constraint */
+ public final SegmentType sourceSegmentTypeX;
+
+ /** The vertical position of the source of the constraint */
+ public final SegmentType sourceSegmentTypeY;
+
+ /** The horizontal position of the target of the constraint */
+ public final SegmentType targetSegmentTypeX;
+
+ /** The vertical position of the target of the constraint */
+ public final SegmentType targetSegmentTypeY;
+
+ /**
+ * If true, the constraint targets the parent layout, otherwise it targets another
+ * view
+ */
+ public final boolean targetParent;
+
+ /** If true, this constraint affects the horizontal dimension */
+ public final boolean horizontalEdge;
+
+ /** If true, this constraint affects the vertical dimension */
+ public final boolean verticalEdge;
+
+ /**
+ * Whether this constraint is relative to the margin bounds of the node rather than
+ * the node's actual bounds
+ */
+ public final boolean relativeToMargin;
+
+ /** Map from attribute name to constraint type */
+ private static Map<String, ConstraintType> sNameToType;
+
+ /**
+ * Returns the {@link ConstraintType} corresponding to the given attribute name, or
+ * null if not found.
+ *
+ * @param attribute the name of the attribute to look up
+ * @return the corresponding {@link ConstraintType}
+ */
+ @Nullable
+ public static ConstraintType fromAttribute(@NonNull String attribute) {
+ if (sNameToType == null) {
+ ConstraintType[] types = ConstraintType.values();
+ Map<String, ConstraintType> map = new HashMap<String, ConstraintType>(types.length);
+ for (ConstraintType type : types) {
+ map.put(type.name, type);
+ }
+ sNameToType = map;
+ }
+ return sNameToType.get(attribute);
+ }
+
+ /**
+ * Returns true if this constraint type represents a constraint where the target edge
+ * is one of the parent edges (actual edge, not center/baseline segments)
+ *
+ * @return true if the target segment is a parent edge
+ */
+ public boolean isRelativeToParentEdge() {
+ return this == ALIGN_PARENT_LEFT || this == ALIGN_PARENT_RIGHT || this == ALIGN_PARENT_TOP
+ || this == ALIGN_PARENT_BOTTOM;
+ }
+
+ /**
+ * Returns a {@link ConstraintType} for a potential match of edges.
+ *
+ * @param withParent if true, the target is the parent
+ * @param from the source edge
+ * @param to the target edge
+ * @return a {@link ConstraintType}, or null
+ */
+ @Nullable
+ public static ConstraintType forMatch(boolean withParent, SegmentType from, SegmentType to) {
+ // Attached to parent edge?
+ if (withParent) {
+ switch (from) {
+ case TOP:
+ return ALIGN_PARENT_TOP;
+ case BOTTOM:
+ return ALIGN_PARENT_BOTTOM;
+ case LEFT:
+ return ALIGN_PARENT_LEFT;
+ case RIGHT:
+ return ALIGN_PARENT_RIGHT;
+ case CENTER_HORIZONTAL:
+ return LAYOUT_CENTER_VERTICAL;
+ case CENTER_VERTICAL:
+ return LAYOUT_CENTER_HORIZONTAL;
+ }
+
+ return null;
+ }
+
+ // Attached to some other node.
+ switch (from) {
+ case TOP:
+ switch (to) {
+ case TOP:
+ return ALIGN_TOP;
+ case BOTTOM:
+ return LAYOUT_BELOW;
+ case BASELINE:
+ return ALIGN_BASELINE;
+ }
+ break;
+ case BOTTOM:
+ switch (to) {
+ case TOP:
+ return LAYOUT_ABOVE;
+ case BOTTOM:
+ return ALIGN_BOTTOM;
+ case BASELINE:
+ return ALIGN_BASELINE;
+ }
+ break;
+ case LEFT:
+ switch (to) {
+ case LEFT:
+ return ALIGN_LEFT;
+ case RIGHT:
+ return LAYOUT_RIGHT_OF;
+ }
+ break;
+ case RIGHT:
+ switch (to) {
+ case LEFT:
+ return LAYOUT_LEFT_OF;
+ case RIGHT:
+ return ALIGN_RIGHT;
+ }
+ break;
+ case BASELINE:
+ return ALIGN_BASELINE;
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DeletionHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DeletionHandler.java
new file mode 100644
index 000000000..3eac510df
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DeletionHandler.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.common.layout.relative;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.ide.common.layout.BaseViewRule.stripIdPrefix;
+import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_CENTER_HORIZONTAL;
+import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_CENTER_VERTICAL;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.INode.IAttribute;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Handles deletions in a relative layout, transferring constraints across
+ * deleted nodes
+ * <p>
+ * TODO: Consider adding the
+ * {@link SdkConstants#ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING} attribute to a
+ * node if it's pointing to a node which is deleted and which has no transitive
+ * reference to another node.
+ */
+public class DeletionHandler {
+ private final INode mLayout;
+ private final INode[] mChildren;
+ private final List<INode> mDeleted;
+ private final Set<String> mDeletedIds;
+ private final Map<String, INode> mNodeMap;
+ private final List<INode> mMoved;
+
+ /**
+ * Creates a new {@link DeletionHandler}
+ *
+ * @param deleted the deleted nodes
+ * @param moved nodes that were moved (e.g. deleted, but also inserted elsewhere)
+ * @param layout the parent layout of the deleted nodes
+ */
+ public DeletionHandler(@NonNull List<INode> deleted, @NonNull List<INode> moved,
+ @NonNull INode layout) {
+ mDeleted = deleted;
+ mMoved = moved;
+ mLayout = layout;
+
+ mChildren = mLayout.getChildren();
+ mNodeMap = Maps.newHashMapWithExpectedSize(mChildren.length);
+ for (INode child : mChildren) {
+ String id = child.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id != null) {
+ mNodeMap.put(stripIdPrefix(id), child);
+ }
+ }
+
+ mDeletedIds = Sets.newHashSetWithExpectedSize(mDeleted.size());
+ for (INode node : mDeleted) {
+ String id = node.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id != null) {
+ mDeletedIds.add(stripIdPrefix(id));
+ }
+ }
+
+ // Any widgets that remain (e.g. typically because they were moved) should
+ // keep their incoming dependencies
+ for (INode node : mMoved) {
+ String id = node.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id != null) {
+ mDeletedIds.remove(stripIdPrefix(id));
+ }
+ }
+ }
+
+ @Nullable
+ private static String getId(@NonNull IAttribute attribute) {
+ if (attribute.getName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
+ && ANDROID_URI.equals(attribute.getUri())
+ && !attribute.getName().startsWith(ATTR_LAYOUT_MARGIN)) {
+ String id = attribute.getValue();
+ // It might not be an id reference, so check manually rather than just
+ // calling stripIdPrefix():
+ if (id.startsWith(NEW_ID_PREFIX)) {
+ return id.substring(NEW_ID_PREFIX.length());
+ } else if (id.startsWith(ID_PREFIX)) {
+ return id.substring(ID_PREFIX.length());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Updates the constraints in the layout to handle deletion of a set of
+ * nodes. This ensures that any constraints pointing to one of the deleted
+ * nodes are changed properly to point to a non-deleted node with similar
+ * constraints.
+ */
+ public void updateConstraints() {
+ if (mChildren.length == mDeleted.size()) {
+ // Deleting everything: Nothing to be done
+ return;
+ }
+
+ // Now remove incoming edges to any views that were deleted. If possible,
+ // don't just delete them but replace them with a transitive constraint, e.g.
+ // if we have "A <= B <= C" and "B" is removed, then we end up with "A <= C",
+
+ for (INode child : mChildren) {
+ if (mDeleted.contains(child)) {
+ continue;
+ }
+
+ for (IAttribute attribute : child.getLiveAttributes()) {
+ String id = getId(attribute);
+ if (id != null) {
+ if (mDeletedIds.contains(id)) {
+ // Unset this reference to a deleted widget. It might be
+ // replaced if the pointed to node points to some other node
+ // on the same side, but it may use a different constraint name,
+ // or have none at all (e.g. parent).
+ String name = attribute.getName();
+ child.setAttribute(ANDROID_URI, name, null);
+
+ INode deleted = mNodeMap.get(id);
+ if (deleted != null) {
+ ConstraintType type = ConstraintType.fromAttribute(name);
+ if (type != null) {
+ transfer(deleted, child, type, 0);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void transfer(INode deleted, INode target, ConstraintType targetType, int depth) {
+ if (depth == 20) {
+ // Prevent really deep flow or unbounded recursion in case there is a bug in
+ // the cycle detection code
+ return;
+ }
+
+ assert mDeleted.contains(deleted);
+
+ for (IAttribute attribute : deleted.getLiveAttributes()) {
+ String name = attribute.getName();
+ ConstraintType type = ConstraintType.fromAttribute(name);
+ if (type == null) {
+ continue;
+ }
+
+ ConstraintType transfer = getCompatibleConstraint(type, targetType);
+ if (transfer != null) {
+ String id = getId(attribute);
+ if (id != null) {
+ if (mDeletedIds.contains(id)) {
+ INode nextDeleted = mNodeMap.get(id);
+ if (nextDeleted != null) {
+ // Points to another deleted node: recurse
+ transfer(nextDeleted, target, targetType, depth + 1);
+ }
+ } else {
+ // Found an undeleted node destination: point to it directly.
+ // Note that we're using the
+ target.setAttribute(ANDROID_URI, transfer.name, attribute.getValue());
+ }
+ } else {
+ // Pointing to parent or center etc (non-id ref): replicate this on the target
+ target.setAttribute(ANDROID_URI, name, attribute.getValue());
+ }
+ }
+ }
+ }
+
+ /**
+ * Determines if two constraints are in the same direction and if so returns
+ * the constraint in the same direction. Rather than returning boolean true
+ * or false, this returns the constraint which is sometimes modified. For
+ * example, if you have a node which points left to a node which is centered
+ * in parent, then the constraint is turned into center horizontal.
+ */
+ @Nullable
+ private static ConstraintType getCompatibleConstraint(
+ @NonNull ConstraintType first, @NonNull ConstraintType second) {
+ if (first == second) {
+ return first;
+ }
+
+ switch (second) {
+ case ALIGN_LEFT:
+ case LAYOUT_RIGHT_OF:
+ switch (first) {
+ case LAYOUT_CENTER_HORIZONTAL:
+ case LAYOUT_LEFT_OF:
+ case ALIGN_LEFT:
+ return first;
+ case LAYOUT_CENTER_IN_PARENT:
+ return LAYOUT_CENTER_HORIZONTAL;
+ }
+ return null;
+
+ case ALIGN_RIGHT:
+ case LAYOUT_LEFT_OF:
+ switch (first) {
+ case LAYOUT_CENTER_HORIZONTAL:
+ case ALIGN_RIGHT:
+ case LAYOUT_LEFT_OF:
+ return first;
+ case LAYOUT_CENTER_IN_PARENT:
+ return LAYOUT_CENTER_HORIZONTAL;
+ }
+ return null;
+
+ case ALIGN_TOP:
+ case LAYOUT_BELOW:
+ case ALIGN_BASELINE:
+ switch (first) {
+ case LAYOUT_CENTER_VERTICAL:
+ case ALIGN_TOP:
+ case LAYOUT_BELOW:
+ case ALIGN_BASELINE:
+ return first;
+ case LAYOUT_CENTER_IN_PARENT:
+ return LAYOUT_CENTER_VERTICAL;
+ }
+ return null;
+ case ALIGN_BOTTOM:
+ case LAYOUT_ABOVE:
+ switch (first) {
+ case LAYOUT_CENTER_VERTICAL:
+ case ALIGN_BOTTOM:
+ case LAYOUT_ABOVE:
+ case ALIGN_BASELINE:
+ return first;
+ case LAYOUT_CENTER_IN_PARENT:
+ return LAYOUT_CENTER_VERTICAL;
+ }
+ return null;
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java
new file mode 100644
index 000000000..43d52d137
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.common.layout.relative;
+
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.VALUE_TRUE;
+
+
+import com.android.SdkConstants;
+import static com.android.SdkConstants.ANDROID_URI;
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.IDragElement.IDragAttribute;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.INode.IAttribute;
+import com.android.ide.common.layout.BaseLayoutRule;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Data structure about relative layout relationships which makes it possible to:
+ * <ul>
+ * <li> Quickly determine not just the dependencies on other nodes, but which nodes
+ * depend on this node such that they can be visualized for the selection
+ * <li> Determine if there are cyclic dependencies, and whether a potential move
+ * would result in a cycle
+ * <li> Determine the "depth" of a given node (in terms of how many connections it
+ * is away from a parent edge) such that we can prioritize connections which
+ * minimizes the depth
+ * </ul>
+ */
+class DependencyGraph {
+ /** Format to chain include cycles in: a=>b=>c=>d etc */
+ static final String CHAIN_FORMAT = "%1$s=>%2$s"; //$NON-NLS-1$
+
+ /** Format to chain constraint dependencies: button 1 above button2 etc */
+ private static final String DEPENDENCY_FORMAT = "%1$s %2$s %3$s"; //$NON-NLS-1$
+
+ private final Map<String, ViewData> mIdToView = new HashMap<String, ViewData>();
+ private final Map<INode, ViewData> mNodeToView = new HashMap<INode, ViewData>();
+
+ /** Constructs a new {@link DependencyGraph} for the given relative layout */
+ DependencyGraph(INode layout) {
+ INode[] nodes = layout.getChildren();
+
+ // Parent view:
+ String parentId = layout.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (parentId != null) {
+ parentId = BaseLayoutRule.stripIdPrefix(parentId);
+ } else {
+ parentId = "RelativeLayout"; // For display purposes; we never reference
+ // the parent id from a constraint, only via parent-relative params
+ // like centerInParent
+ }
+ ViewData parentView = new ViewData(layout, parentId);
+ mNodeToView.put(layout, parentView);
+ if (parentId != null) {
+ mIdToView.put(parentId, parentView);
+ }
+
+ for (INode child : nodes) {
+ String id = child.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id != null) {
+ id = BaseLayoutRule.stripIdPrefix(id);
+ }
+ ViewData view = new ViewData(child, id);
+ mNodeToView.put(child, view);
+ if (id != null) {
+ mIdToView.put(id, view);
+ }
+ }
+
+ for (ViewData view : mNodeToView.values()) {
+ for (IAttribute attribute : view.node.getLiveAttributes()) {
+ String name = attribute.getName();
+ ConstraintType type = ConstraintType.fromAttribute(name);
+ if (type != null) {
+ String value = attribute.getValue();
+
+ if (type.targetParent) {
+ if (value.equals(VALUE_TRUE)) {
+ Constraint constraint = new Constraint(type, view, parentView);
+ view.dependsOn.add(constraint);
+ parentView.dependedOnBy.add(constraint);
+ }
+ } else {
+ // id-based constraint.
+ // NOTE: The id could refer to some widget that is NOT a sibling!
+ String targetId = BaseLayoutRule.stripIdPrefix(value);
+ ViewData target = mIdToView.get(targetId);
+ if (target == view) {
+ // Self-reference. RelativeLayout ignores these so it's
+ // not an error like a deeper cycle (where RelativeLayout
+ // will throw an exception), but we might as well warn
+ // the user about it.
+ // TODO: Where do we emit this error?
+ } else if (target != null) {
+ Constraint constraint = new Constraint(type, view, target);
+ view.dependsOn.add(constraint);
+ target.dependedOnBy.add(constraint);
+ } else {
+ // This is valid but we might want to warn...
+ //System.out.println("Warning: no view data found for " + targetId);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public ViewData getView(IDragElement element) {
+ IDragAttribute attribute = element.getAttribute(ANDROID_URI, ATTR_ID);
+ if (attribute != null) {
+ String id = attribute.getValue();
+ id = BaseLayoutRule.stripIdPrefix(id);
+ return getView(id);
+ }
+
+ return null;
+ }
+
+ public ViewData getView(String id) {
+ return mIdToView.get(id);
+ }
+
+ public ViewData getView(INode node) {
+ return mNodeToView.get(node);
+ }
+
+ /**
+ * Returns the set of views that depend on the given node in either the horizontal or
+ * vertical direction
+ *
+ * @param nodes the set of nodes that we want to compute the transitive dependencies
+ * for
+ * @param vertical if true, look for vertical edge dependencies, otherwise look for
+ * horizontal edge dependencies
+ * @return the set of nodes that directly or indirectly depend on the given nodes in
+ * the given direction
+ */
+ public Set<INode> dependsOn(Collection<? extends INode> nodes, boolean vertical) {
+ List<ViewData> reachable = new ArrayList<ViewData>();
+
+ // Traverse the graph of constraints and determine all nodes affected by
+ // this node
+ Set<ViewData> visiting = new HashSet<ViewData>();
+ for (INode node : nodes) {
+ ViewData view = mNodeToView.get(node);
+ if (view != null) {
+ findBackwards(view, visiting, reachable, vertical, view);
+ }
+ }
+
+ Set<INode> dependents = new HashSet<INode>(reachable.size());
+
+ for (ViewData v : reachable) {
+ dependents.add(v.node);
+ }
+
+ return dependents;
+ }
+
+ private void findBackwards(ViewData view,
+ Set<ViewData> visiting, List<ViewData> reachable,
+ boolean vertical, ViewData start) {
+ visiting.add(view);
+ reachable.add(view);
+
+ for (Constraint constraint : view.dependedOnBy) {
+ if (vertical && !constraint.type.verticalEdge) {
+ continue;
+ } else if (!vertical && !constraint.type.horizontalEdge) {
+ continue;
+ }
+
+ assert constraint.to == view;
+ ViewData from = constraint.from;
+ if (visiting.contains(from)) {
+ // Cycle - what do we do to highlight this?
+ List<Constraint> path = getPathTo(start.node, view.node, vertical);
+ if (path != null) {
+ // TODO: display to the user somehow. We need log access for the
+ // view rules.
+ System.out.println(Constraint.describePath(path, null, null));
+ }
+ } else {
+ findBackwards(from, visiting, reachable, vertical, start);
+ }
+ }
+
+ visiting.remove(view);
+ }
+
+ public List<Constraint> getPathTo(INode from, INode to, boolean vertical) {
+ // Traverse the graph of constraints and determine all nodes affected by
+ // this node
+ Set<ViewData> visiting = new HashSet<ViewData>();
+ List<Constraint> path = new ArrayList<Constraint>();
+ ViewData view = mNodeToView.get(from);
+ if (view != null) {
+ return findForwards(view, visiting, path, vertical, to);
+ }
+
+ return null;
+ }
+
+ private List<Constraint> findForwards(ViewData view, Set<ViewData> visiting,
+ List<Constraint> path, boolean vertical, INode target) {
+ visiting.add(view);
+
+ for (Constraint constraint : view.dependsOn) {
+ if (vertical && !constraint.type.verticalEdge) {
+ continue;
+ } else if (!vertical && !constraint.type.horizontalEdge) {
+ continue;
+ }
+
+ try {
+ path.add(constraint);
+
+ if (constraint.to.node == target) {
+ return new ArrayList<Constraint>(path);
+ }
+
+ assert constraint.from == view;
+ ViewData to = constraint.to;
+ if (visiting.contains(to)) {
+ // CYCLE!
+ continue;
+ }
+
+ List<Constraint> chain = findForwards(to, visiting, path, vertical, target);
+ if (chain != null) {
+ return chain;
+ }
+ } finally {
+ path.remove(constraint);
+ }
+ }
+
+ visiting.remove(view);
+
+ return null;
+ }
+
+ /**
+ * Info about a specific widget child of a relative layout and its constraints. This
+ * is a node in the dependency graph.
+ */
+ static class ViewData {
+ public final INode node;
+ public final String id;
+ public final List<Constraint> dependsOn = new ArrayList<Constraint>(4);
+ public final List<Constraint> dependedOnBy = new ArrayList<Constraint>(8);
+
+ ViewData(INode node, String id) {
+ this.node = node;
+ this.id = id;
+ }
+ }
+
+ /**
+ * Info about a specific constraint between two widgets in a relative layout. This is
+ * an edge in the dependency graph.
+ */
+ static class Constraint {
+ public final ConstraintType type;
+ public final ViewData from;
+ public final ViewData to;
+
+ // TODO: Initialize depth -- should be computed independently for top, left, etc.
+ // We can use this in GuidelineHandler.MatchComparator to prefer matches that
+ // are closer to a parent edge:
+ //public int depth;
+
+ Constraint(ConstraintType type, ViewData from, ViewData to) {
+ this.type = type;
+ this.from = from;
+ this.to = to;
+ }
+
+ static String describePath(List<Constraint> path, String newName, String newId) {
+ String s = "";
+ for (int i = path.size() - 1; i >= 0; i--) {
+ Constraint constraint = path.get(i);
+ String suffix = (i == path.size() -1) ? constraint.to.id : s;
+ s = String.format(DEPENDENCY_FORMAT, constraint.from.id,
+ stripLayoutAttributePrefix(constraint.type.name), suffix);
+ }
+
+ if (newName != null) {
+ s = String.format(DEPENDENCY_FORMAT, s, stripLayoutAttributePrefix(newName),
+ BaseLayoutRule.stripIdPrefix(newId));
+ }
+
+ return s;
+ }
+
+ private static String stripLayoutAttributePrefix(String name) {
+ if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
+ return name.substring(ATTR_LAYOUT_RESOURCE_PREFIX.length());
+ }
+
+ return name;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java
new file mode 100644
index 000000000..db08b1857
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java
@@ -0,0 +1,839 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.common.layout.relative;
+
+import static com.android.ide.common.api.MarginType.NO_MARGIN;
+import static com.android.ide.common.api.MarginType.WITHOUT_MARGIN;
+import static com.android.ide.common.api.MarginType.WITH_MARGIN;
+import static com.android.ide.common.api.SegmentType.BASELINE;
+import static com.android.ide.common.api.SegmentType.BOTTOM;
+import static com.android.ide.common.api.SegmentType.CENTER_HORIZONTAL;
+import static com.android.ide.common.api.SegmentType.CENTER_VERTICAL;
+import static com.android.ide.common.api.SegmentType.LEFT;
+import static com.android.ide.common.api.SegmentType.RIGHT;
+import static com.android.ide.common.api.SegmentType.TOP;
+import static com.android.ide.common.layout.BaseLayoutRule.getMaxMatchDistance;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_IN_PARENT;
+import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
+import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
+import static com.android.SdkConstants.VALUE_N_DP;
+import static com.android.SdkConstants.VALUE_TRUE;
+import static com.android.ide.common.layout.relative.ConstraintType.ALIGN_BASELINE;
+
+import static java.lang.Math.abs;
+
+import com.android.SdkConstants;
+import static com.android.SdkConstants.ANDROID_URI;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IClientRulesEngine;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.Segment;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.common.layout.BaseLayoutRule;
+import com.android.ide.common.layout.relative.DependencyGraph.Constraint;
+import com.android.ide.common.layout.relative.DependencyGraph.ViewData;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The {@link GuidelineHandler} class keeps track of state related to a guideline operation
+ * like move and resize, and performs various constraint computations.
+ */
+public class GuidelineHandler {
+ /**
+ * A dependency graph for the relative layout recording constraint relationships
+ */
+ protected DependencyGraph mDependencyGraph;
+
+ /** The RelativeLayout we are moving/resizing within */
+ public INode layout;
+
+ /** The set of nodes being dragged (may be null) */
+ protected Collection<INode> mDraggedNodes;
+
+ /** The bounds of the primary child node being dragged */
+ protected Rect mBounds;
+
+ /** Whether the left edge is being moved/resized */
+ protected boolean mMoveLeft;
+
+ /** Whether the right edge is being moved/resized */
+ protected boolean mMoveRight;
+
+ /** Whether the top edge is being moved/resized */
+ protected boolean mMoveTop;
+
+ /** Whether the bottom edge is being moved/resized */
+ protected boolean mMoveBottom;
+
+ /**
+ * Whether the drop/move/resize position should be snapped (which can be turned off
+ * with a modifier key during the operation)
+ */
+ protected boolean mSnap = true;
+
+ /**
+ * The set of nodes which depend on the currently selected nodes, including
+ * transitively, through horizontal constraints (a "horizontal constraint"
+ * is a constraint between two horizontal edges)
+ */
+ protected Set<INode> mHorizontalDeps;
+
+ /**
+ * The set of nodes which depend on the currently selected nodes, including
+ * transitively, through vertical constraints (a "vertical constraint"
+ * is a constraint between two vertical edges)
+ */
+ protected Set<INode> mVerticalDeps;
+
+ /** The current list of constraints which result in a horizontal cycle (if applicable) */
+ protected List<Constraint> mHorizontalCycle;
+
+ /** The current list of constraints which result in a vertical cycle (if applicable) */
+ protected List<Constraint> mVerticalCycle;
+
+ /**
+ * All horizontal segments in the relative layout - top and bottom edges, baseline
+ * edges, and top and bottom edges offset by the applicable margins in each direction
+ */
+ protected List<Segment> mHorizontalEdges;
+
+ /**
+ * All vertical segments in the relative layout - left and right edges, and left and
+ * right edges offset by the applicable margins in each direction
+ */
+ protected List<Segment> mVerticalEdges;
+
+ /**
+ * All center vertical segments in the relative layout. These are kept separate since
+ * they only match other center edges.
+ */
+ protected List<Segment> mCenterVertEdges;
+
+ /**
+ * All center horizontal segments in the relative layout. These are kept separate
+ * since they only match other center edges.
+ */
+ protected List<Segment> mCenterHorizEdges;
+
+ /**
+ * Suggestions for horizontal matches. There could be more than one, but all matches
+ * will be equidistant from the current position (as well as in the same direction,
+ * which means that you can't have one match 5 pixels to the left and one match 5
+ * pixels to the right since it would be impossible to snap to fit with both; you can
+ * however have multiple matches all 5 pixels to the left.)
+ * <p
+ * The best vertical match will be found in {@link #mCurrentTopMatch} or
+ * {@link #mCurrentBottomMatch}.
+ */
+ protected List<Match> mHorizontalSuggestions;
+
+ /**
+ * Suggestions for vertical matches.
+ * <p
+ * The best vertical match will be found in {@link #mCurrentLeftMatch} or
+ * {@link #mCurrentRightMatch}.
+ */
+ protected List<Match> mVerticalSuggestions;
+
+ /**
+ * The current match on the left edge, or null if no match or if the left edge is not
+ * being moved or resized.
+ */
+ protected Match mCurrentLeftMatch;
+
+ /**
+ * The current match on the top edge, or null if no match or if the top edge is not
+ * being moved or resized.
+ */
+ protected Match mCurrentTopMatch;
+
+ /**
+ * The current match on the right edge, or null if no match or if the right edge is
+ * not being moved or resized.
+ */
+ protected Match mCurrentRightMatch;
+
+ /**
+ * The current match on the bottom edge, or null if no match or if the bottom edge is
+ * not being moved or resized.
+ */
+ protected Match mCurrentBottomMatch;
+
+ /**
+ * The amount of margin to add to the top edge, or 0
+ */
+ protected int mTopMargin;
+
+ /**
+ * The amount of margin to add to the bottom edge, or 0
+ */
+ protected int mBottomMargin;
+
+ /**
+ * The amount of margin to add to the left edge, or 0
+ */
+ protected int mLeftMargin;
+
+ /**
+ * The amount of margin to add to the right edge, or 0
+ */
+ protected int mRightMargin;
+
+ /**
+ * The associated rules engine
+ */
+ protected IClientRulesEngine mRulesEngine;
+
+ /**
+ * Construct a new {@link GuidelineHandler} for the given relative layout.
+ *
+ * @param layout the RelativeLayout to handle
+ */
+ GuidelineHandler(INode layout, IClientRulesEngine rulesEngine) {
+ this.layout = layout;
+ mRulesEngine = rulesEngine;
+
+ mHorizontalEdges = new ArrayList<Segment>();
+ mVerticalEdges = new ArrayList<Segment>();
+ mCenterVertEdges = new ArrayList<Segment>();
+ mCenterHorizEdges = new ArrayList<Segment>();
+ mDependencyGraph = new DependencyGraph(layout);
+ }
+
+ /**
+ * Returns true if the handler has any suggestions to offer
+ *
+ * @return true if the handler has any suggestions to offer
+ */
+ public boolean haveSuggestions() {
+ return mCurrentLeftMatch != null || mCurrentTopMatch != null
+ || mCurrentRightMatch != null || mCurrentBottomMatch != null;
+ }
+
+ /**
+ * Returns the closest match.
+ *
+ * @return the closest match, or null if nothing matched
+ */
+ protected Match pickBestMatch(List<Match> matches) {
+ int alternatives = matches.size();
+ if (alternatives == 0) {
+ return null;
+ } else if (alternatives == 1) {
+ Match match = matches.get(0);
+ return match;
+ } else {
+ assert alternatives > 1;
+ Collections.sort(matches, new MatchComparator());
+ return matches.get(0);
+ }
+ }
+
+ private boolean checkCycle(DropFeedback feedback, Match match, boolean vertical) {
+ if (match != null && match.cycle) {
+ for (INode node : mDraggedNodes) {
+ INode from = match.edge.node;
+ assert match.with.node == null || match.with.node == node;
+ INode to = node;
+ List<Constraint> path = mDependencyGraph.getPathTo(from, to, vertical);
+ if (path != null) {
+ if (vertical) {
+ mVerticalCycle = path;
+ } else {
+ mHorizontalCycle = path;
+ }
+ String desc = Constraint.describePath(path,
+ match.type.name, match.edge.id);
+
+ feedback.errorMessage = "Constraint creates a cycle: " + desc;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks for any cycles in the dependencies
+ *
+ * @param feedback the drop feedback state
+ */
+ public void checkCycles(DropFeedback feedback) {
+ // Deliberate short circuit evaluation -- only list the first cycle
+ feedback.errorMessage = null;
+ mHorizontalCycle = null;
+ mVerticalCycle = null;
+
+ if (checkCycle(feedback, mCurrentTopMatch, true /* vertical */)
+ || checkCycle(feedback, mCurrentBottomMatch, true)) {
+ }
+
+ if (checkCycle(feedback, mCurrentLeftMatch, false)
+ || checkCycle(feedback, mCurrentRightMatch, false)) {
+ }
+ }
+
+ /** Records the matchable outside edges for the given node to the potential match list */
+ protected void addBounds(INode node, String id,
+ boolean addHorizontal, boolean addVertical) {
+ Rect b = node.getBounds();
+ Margins margins = node.getMargins();
+ if (addHorizontal) {
+ if (margins.top != 0) {
+ mHorizontalEdges.add(new Segment(b.y, b.x, b.x2(), node, id, TOP, WITHOUT_MARGIN));
+ mHorizontalEdges.add(new Segment(b.y - margins.top, b.x, b.x2(), node, id,
+ TOP, WITH_MARGIN));
+ } else {
+ mHorizontalEdges.add(new Segment(b.y, b.x, b.x2(), node, id, TOP, NO_MARGIN));
+ }
+ if (margins.bottom != 0) {
+ mHorizontalEdges.add(new Segment(b.y2(), b.x, b.x2(), node, id, BOTTOM,
+ WITHOUT_MARGIN));
+ mHorizontalEdges.add(new Segment(b.y2() + margins.bottom, b.x, b.x2(), node,
+ id, BOTTOM, WITH_MARGIN));
+ } else {
+ mHorizontalEdges.add(new Segment(b.y2(), b.x, b.x2(), node, id,
+ BOTTOM, NO_MARGIN));
+ }
+ }
+ if (addVertical) {
+ if (margins.left != 0) {
+ mVerticalEdges.add(new Segment(b.x, b.y, b.y2(), node, id, LEFT, WITHOUT_MARGIN));
+ mVerticalEdges.add(new Segment(b.x - margins.left, b.y, b.y2(), node, id, LEFT,
+ WITH_MARGIN));
+ } else {
+ mVerticalEdges.add(new Segment(b.x, b.y, b.y2(), node, id, LEFT, NO_MARGIN));
+ }
+
+ if (margins.right != 0) {
+ mVerticalEdges.add(new Segment(b.x2(), b.y, b.y2(), node, id,
+ RIGHT, WITHOUT_MARGIN));
+ mVerticalEdges.add(new Segment(b.x2() + margins.right, b.y, b.y2(), node, id,
+ RIGHT, WITH_MARGIN));
+ } else {
+ mVerticalEdges.add(new Segment(b.x2(), b.y, b.y2(), node, id,
+ RIGHT, NO_MARGIN));
+ }
+ }
+ }
+
+ /** Records the center edges for the given node to the potential match list */
+ protected void addCenter(INode node, String id,
+ boolean addHorizontal, boolean addVertical) {
+ Rect b = node.getBounds();
+
+ if (addHorizontal) {
+ mCenterHorizEdges.add(new Segment(b.centerY(), b.x, b.x2(),
+ node, id, CENTER_HORIZONTAL, NO_MARGIN));
+ }
+ if (addVertical) {
+ mCenterVertEdges.add(new Segment(b.centerX(), b.y, b.y2(),
+ node, id, CENTER_VERTICAL, NO_MARGIN));
+ }
+ }
+
+ /** Records the baseline edge for the given node to the potential match list */
+ protected int addBaseLine(INode node, String id) {
+ int baselineY = node.getBaseline();
+ if (baselineY != -1) {
+ Rect b = node.getBounds();
+ mHorizontalEdges.add(new Segment(b.y + baselineY, b.x, b.x2(), node, id, BASELINE,
+ NO_MARGIN));
+ }
+
+ return baselineY;
+ }
+
+ protected void snapVertical(Segment vEdge, int x, Rect newBounds) {
+ newBounds.x = x;
+ }
+
+ protected void snapHorizontal(Segment hEdge, int y, Rect newBounds) {
+ newBounds.y = y;
+ }
+
+ /**
+ * Returns whether two edge types are compatible. For example, we only match the
+ * center of one object with the center of another.
+ *
+ * @param edge the first edge type to compare
+ * @param dragged the second edge type to compare the first one with
+ * @param delta the delta between the two edge locations
+ * @return true if the two edge types can be compatibly matched
+ */
+ protected boolean isEdgeTypeCompatible(SegmentType edge, SegmentType dragged, int delta) {
+
+ if (Math.abs(delta) > BaseLayoutRule.getMaxMatchDistance()) {
+ if (dragged == LEFT || dragged == TOP) {
+ if (delta > 0) {
+ return false;
+ }
+ } else {
+ if (delta < 0) {
+ return false;
+ }
+ }
+ }
+
+ switch (edge) {
+ case BOTTOM:
+ case TOP:
+ return dragged == TOP || dragged == BOTTOM;
+ case LEFT:
+ case RIGHT:
+ return dragged == LEFT || dragged == RIGHT;
+
+ // Center horizontal, center vertical and Baseline only matches the same
+ // type, and only within the matching distance -- no margins!
+ case BASELINE:
+ case CENTER_HORIZONTAL:
+ case CENTER_VERTICAL:
+ return dragged == edge && Math.abs(delta) < getMaxMatchDistance();
+ default: assert false : edge;
+ }
+ return false;
+ }
+
+ /**
+ * Finds the closest matching segments among the given list of edges for the given
+ * dragged edge, and returns these as a list of matches
+ */
+ protected List<Match> findClosest(Segment draggedEdge, List<Segment> edges) {
+ List<Match> closest = new ArrayList<Match>();
+ addClosest(draggedEdge, edges, closest);
+ return closest;
+ }
+
+ protected void addClosest(Segment draggedEdge, List<Segment> edges,
+ List<Match> closest) {
+ int at = draggedEdge.at;
+ int closestDelta = closest.size() > 0 ? closest.get(0).delta : Integer.MAX_VALUE;
+ int closestDistance = abs(closestDelta);
+ for (Segment edge : edges) {
+ assert draggedEdge.edgeType.isHorizontal() == edge.edgeType.isHorizontal();
+
+ int delta = edge.at - at;
+ int distance = abs(delta);
+ if (distance > closestDistance) {
+ continue;
+ }
+
+ if (!isEdgeTypeCompatible(edge.edgeType, draggedEdge.edgeType, delta)) {
+ continue;
+ }
+
+ boolean withParent = edge.node == layout;
+ ConstraintType type = ConstraintType.forMatch(withParent,
+ draggedEdge.edgeType, edge.edgeType);
+ if (type == null) {
+ continue;
+ }
+
+ // Ensure that the edge match is compatible; for example, a "below"
+ // constraint can only apply to the margin bounds and a "bottom"
+ // constraint can only apply to the non-margin bounds.
+ if (type.relativeToMargin && edge.marginType == WITHOUT_MARGIN) {
+ continue;
+ } else if (!type.relativeToMargin && edge.marginType == WITH_MARGIN) {
+ continue;
+ }
+
+ Match match = new Match(this, edge, draggedEdge, type, delta);
+
+ if (distance < closestDistance) {
+ closest.clear();
+ closestDistance = distance;
+ closestDelta = delta;
+ } else if (delta * closestDelta < 0) {
+ // They have different signs, e.g. the matches are equal but
+ // on opposite sides; can't accept them both
+ continue;
+ }
+ closest.add(match);
+ }
+ }
+
+ protected void clearSuggestions() {
+ mHorizontalSuggestions = mVerticalSuggestions = null;
+ mCurrentLeftMatch = mCurrentRightMatch = null;
+ mCurrentTopMatch = mCurrentBottomMatch = null;
+ }
+
+ /**
+ * Given a node, apply the suggestions by expressing them as relative layout param
+ * values
+ *
+ * @param n the node to apply constraints to
+ */
+ public void applyConstraints(INode n) {
+ // Process each edge separately
+ String centerBoth = n.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT);
+ if (centerBoth != null && centerBoth.equals(VALUE_TRUE)) {
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, null);
+
+ // If you had a center-in-both-directions attribute, and you're
+ // only resizing in one dimension, then leave the other dimension
+ // centered, e.g. if you have centerInParent and apply alignLeft,
+ // then you should end up with alignLeft and centerVertically
+ if (mCurrentTopMatch == null && mCurrentBottomMatch == null) {
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, VALUE_TRUE);
+ }
+ if (mCurrentLeftMatch == null && mCurrentRightMatch == null) {
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, VALUE_TRUE);
+ }
+ }
+
+ if (mMoveTop) {
+ // Remove top attachments
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_TOP, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_TOP, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_BELOW, null);
+
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null);
+
+ }
+
+ if (mMoveBottom) {
+ // Remove bottom attachments
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BOTTOM, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ABOVE, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null);
+ }
+
+ if (mMoveLeft) {
+ // Remove left attachments
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_LEFT, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_LEFT, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
+ }
+
+ if (mMoveRight) {
+ // Remove right attachments
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_RIGHT, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_RIGHT, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF, null);
+ n.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
+ }
+
+ if (mMoveTop && mCurrentTopMatch != null) {
+ applyConstraint(n, mCurrentTopMatch.getConstraint(true /* generateId */));
+ if (mCurrentTopMatch.type == ALIGN_BASELINE) {
+ // HACK! WORKAROUND! Baseline doesn't provide a new bottom edge for attachments
+ String c = mCurrentTopMatch.getConstraint(true);
+ c = c.replace(ATTR_LAYOUT_ALIGN_BASELINE, ATTR_LAYOUT_ALIGN_BOTTOM);
+ applyConstraint(n, c);
+ }
+ }
+
+ if (mMoveBottom && mCurrentBottomMatch != null) {
+ applyConstraint(n, mCurrentBottomMatch.getConstraint(true));
+ }
+
+ if (mMoveLeft && mCurrentLeftMatch != null) {
+ applyConstraint(n, mCurrentLeftMatch.getConstraint(true));
+ }
+
+ if (mMoveRight && mCurrentRightMatch != null) {
+ applyConstraint(n, mCurrentRightMatch.getConstraint(true));
+ }
+
+ if (mMoveLeft) {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_LEFT, mLeftMargin);
+ }
+ if (mMoveRight) {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_RIGHT, mRightMargin);
+ }
+ if (mMoveTop) {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_TOP, mTopMargin);
+ }
+ if (mMoveBottom) {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_BOTTOM, mBottomMargin);
+ }
+ }
+
+ private void applyConstraint(INode n, String constraint) {
+ assert constraint.contains("=") : constraint;
+ String name = constraint.substring(0, constraint.indexOf('='));
+ String value = constraint.substring(constraint.indexOf('=') + 1);
+ n.setAttribute(ANDROID_URI, name, value);
+ }
+
+ private void applyMargin(INode n, String marginAttribute, int margin) {
+ if (margin > 0) {
+ int dp = mRulesEngine.pxToDp(margin);
+ n.setAttribute(ANDROID_URI, marginAttribute, String.format(VALUE_N_DP, dp));
+ } else if (n.getStringAttr(ANDROID_URI, marginAttribute) != null) {
+ // Clear out existing margin
+ n.setAttribute(ANDROID_URI, marginAttribute, null);
+ }
+ }
+
+ private void removeRelativeParams(INode node) {
+ for (ConstraintType type : ConstraintType.values()) {
+ node.setAttribute(ANDROID_URI, type.name, null);
+ }
+ node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_LEFT, null);
+ node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_RIGHT, null);
+ node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_TOP, null);
+ node.setAttribute(ANDROID_URI,ATTR_LAYOUT_MARGIN_BOTTOM, null);
+ }
+
+ /**
+ * Attach the new child to the previous node
+ * @param previous the previous child
+ * @param node the new child to attach it to
+ */
+ public void attachPrevious(INode previous, INode node) {
+ removeRelativeParams(node);
+
+ String id = previous.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id == null) {
+ return;
+ }
+
+ if (mCurrentTopMatch != null || mCurrentBottomMatch != null) {
+ // Attaching the top: arrange below, and for bottom arrange above
+ node.setAttribute(ANDROID_URI,
+ mCurrentTopMatch != null ? ATTR_LAYOUT_BELOW : ATTR_LAYOUT_ABOVE, id);
+ // Apply same left/right constraints as the parent
+ if (mCurrentLeftMatch != null) {
+ applyConstraint(node, mCurrentLeftMatch.getConstraint(true));
+ applyMargin(node, ATTR_LAYOUT_MARGIN_LEFT, mLeftMargin);
+ } else if (mCurrentRightMatch != null) {
+ applyConstraint(node, mCurrentRightMatch.getConstraint(true));
+ applyMargin(node, ATTR_LAYOUT_MARGIN_RIGHT, mRightMargin);
+ }
+ } else if (mCurrentLeftMatch != null || mCurrentRightMatch != null) {
+ node.setAttribute(ANDROID_URI,
+ mCurrentLeftMatch != null ? ATTR_LAYOUT_TO_RIGHT_OF : ATTR_LAYOUT_TO_LEFT_OF,
+ id);
+ // Apply same top/bottom constraints as the parent
+ if (mCurrentTopMatch != null) {
+ applyConstraint(node, mCurrentTopMatch.getConstraint(true));
+ applyMargin(node, ATTR_LAYOUT_MARGIN_TOP, mTopMargin);
+ } else if (mCurrentBottomMatch != null) {
+ applyConstraint(node, mCurrentBottomMatch.getConstraint(true));
+ applyMargin(node, ATTR_LAYOUT_MARGIN_BOTTOM, mBottomMargin);
+ }
+ } else {
+ return;
+ }
+ }
+
+ /** Breaks any cycles detected by the handler */
+ public void removeCycles() {
+ if (mHorizontalCycle != null) {
+ removeCycles(mHorizontalDeps);
+ }
+ if (mVerticalCycle != null) {
+ removeCycles(mVerticalDeps);
+ }
+ }
+
+ private void removeCycles(Set<INode> deps) {
+ for (INode node : mDraggedNodes) {
+ ViewData view = mDependencyGraph.getView(node);
+ if (view != null) {
+ for (Constraint constraint : view.dependedOnBy) {
+ // For now, remove ALL constraints pointing to this node in this orientation.
+ // Later refine this to be smarter. (We can't JUST remove the constraints
+ // identified in the cycle since there could be multiple.)
+ constraint.from.node.setAttribute(ANDROID_URI, constraint.type.name, null);
+ }
+ }
+ }
+ }
+
+ /**
+ * Comparator used to sort matches such that the first match is the most desirable
+ * match (where we prefer attaching to parent bounds, we avoid matches that lead to a
+ * cycle, we prefer constraints on closer widgets rather than ones further away, and
+ * so on.)
+ * <p>
+ * There are a number of sorting criteria. One of them is the distance between the
+ * matched edges. We may end up with multiple matches that are the same distance. In
+ * that case we look at the orientation; on the left side, prefer left-oriented
+ * attachments, and on the right-side prefer right-oriented attachments. For example,
+ * consider the following scenario:
+ *
+ * <pre>
+ * +--------------------+-------------------------+
+ * | Attached on left | |
+ * +--------------------+ |
+ * | |
+ * | +-----+ |
+ * | | A | |
+ * | +-----+ |
+ * | |
+ * | +-------------------------+
+ * | | Attached on right |
+ * +--------------------+-------------------------+
+ * </pre>
+ *
+ * Here, dragging the left edge should attach to the top left attached view, whereas
+ * in the following layout dragging the right edge would attach to the bottom view:
+ *
+ * <pre>
+ * +--------------------------+-------------------+
+ * | Attached on left | |
+ * +--------------------------+ |
+ * | |
+ * | +-----+ |
+ * | | A | |
+ * | +-----+ |
+ * | |
+ * | +-------------------+
+ * | | Attached on right |
+ * +--------------------------+-------------------+
+ *
+ * </pre>
+ *
+ * </ul>
+ */
+ private final class MatchComparator implements Comparator<Match> {
+ @Override
+ public int compare(Match m1, Match m2) {
+ // Always prefer matching parent bounds
+ int parent1 = m1.edge.node == layout ? -1 : 1;
+ int parent2 = m2.edge.node == layout ? -1 : 1;
+ // unless it's a center bound -- those should always get lowest priority since
+ // they overlap with other usually more interesting edges near the center of
+ // the layout.
+ if (m1.edge.edgeType == CENTER_HORIZONTAL
+ || m1.edge.edgeType == CENTER_VERTICAL) {
+ parent1 = 2;
+ }
+ if (m2.edge.edgeType == CENTER_HORIZONTAL
+ || m2.edge.edgeType == CENTER_VERTICAL) {
+ parent2 = 2;
+ }
+ if (parent1 != parent2) {
+ return parent1 - parent2;
+ }
+
+ // Avoid matching edges that would lead to a cycle
+ if (m1.edge.edgeType.isHorizontal()) {
+ int cycle1 = mHorizontalDeps.contains(m1.edge.node) ? 1 : -1;
+ int cycle2 = mHorizontalDeps.contains(m2.edge.node) ? 1 : -1;
+ if (cycle1 != cycle2) {
+ return cycle1 - cycle2;
+ }
+ } else {
+ int cycle1 = mVerticalDeps.contains(m1.edge.node) ? 1 : -1;
+ int cycle2 = mVerticalDeps.contains(m2.edge.node) ? 1 : -1;
+ if (cycle1 != cycle2) {
+ return cycle1 - cycle2;
+ }
+ }
+
+ // TODO: Sort by minimum depth -- do we have the depth anywhere?
+
+ // Prefer nodes that are closer
+ int distance1, distance2;
+ if (m1.edge.to <= m1.with.from) {
+ distance1 = m1.with.from - m1.edge.to;
+ } else if (m1.edge.from >= m1.with.to) {
+ distance1 = m1.edge.from - m1.with.to;
+ } else {
+ // Some kind of overlap - not sure how to prioritize these yet...
+ distance1 = 0;
+ }
+ if (m2.edge.to <= m2.with.from) {
+ distance2 = m2.with.from - m2.edge.to;
+ } else if (m2.edge.from >= m2.with.to) {
+ distance2 = m2.edge.from - m2.with.to;
+ } else {
+ // Some kind of overlap - not sure how to prioritize these yet...
+ distance2 = 0;
+ }
+
+ if (distance1 != distance2) {
+ return distance1 - distance2;
+ }
+
+ // Prefer matching on baseline
+ int baseline1 = (m1.edge.edgeType == BASELINE) ? -1 : 1;
+ int baseline2 = (m2.edge.edgeType == BASELINE) ? -1 : 1;
+ if (baseline1 != baseline2) {
+ return baseline1 - baseline2;
+ }
+
+ // Prefer matching top/left edges before matching bottom/right edges
+ int orientation1 = (m1.with.edgeType == LEFT ||
+ m1.with.edgeType == TOP) ? -1 : 1;
+ int orientation2 = (m2.with.edgeType == LEFT ||
+ m2.with.edgeType == TOP) ? -1 : 1;
+ if (orientation1 != orientation2) {
+ return orientation1 - orientation2;
+ }
+
+ // Prefer opposite-matching over same-matching.
+ // In other words, if we have the choice of matching
+ // our left edge with another element's left edge,
+ // or matching our left edge with another element's right
+ // edge, prefer the right edge since that
+ // The two matches have identical distance; try to sort by
+ // orientation
+ int edgeType1 = (m1.edge.edgeType != m1.with.edgeType) ? -1 : 1;
+ int edgeType2 = (m2.edge.edgeType != m2.with.edgeType) ? -1 : 1;
+ if (edgeType1 != edgeType2) {
+ return edgeType1 - edgeType2;
+ }
+
+ return 0;
+ }
+ }
+
+ /**
+ * Returns the {@link IClientRulesEngine} IDE callback
+ *
+ * @return the {@link IClientRulesEngine} IDE callback, never null
+ */
+ public IClientRulesEngine getRulesEngine() {
+ return mRulesEngine;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelinePainter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelinePainter.java
new file mode 100644
index 000000000..2fe74768f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelinePainter.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.common.layout.relative;
+
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IFeedbackPainter;
+import com.android.ide.common.api.IGraphics;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Point;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.common.layout.relative.DependencyGraph.Constraint;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The {@link GuidelinePainter} is responsible for painting guidelines during an operation
+ * which uses a {@link GuidelineHandler} such as a resize operation.
+ */
+public final class GuidelinePainter implements IFeedbackPainter {
+ // ---- Implements IFeedbackPainter ----
+ @Override
+ public void paint(@NonNull IGraphics gc, @NonNull INode node, @NonNull DropFeedback feedback) {
+ GuidelineHandler state = (GuidelineHandler) feedback.userData;
+
+ for (INode dragged : state.mDraggedNodes) {
+ gc.useStyle(DrawingStyle.DRAGGED);
+ Rect bounds = dragged.getBounds();
+ if (bounds.isValid()) {
+ gc.fillRect(bounds);
+ }
+ }
+
+ Set<INode> horizontalDeps = state.mHorizontalDeps;
+ Set<INode> verticalDeps = state.mVerticalDeps;
+ Set<INode> deps = new HashSet<INode>(horizontalDeps.size() + verticalDeps.size());
+ deps.addAll(horizontalDeps);
+ deps.addAll(verticalDeps);
+ if (deps.size() > 0) {
+ gc.useStyle(DrawingStyle.DEPENDENCY);
+ for (INode n : deps) {
+ // Don't highlight the selected nodes themselves
+ if (state.mDraggedNodes.contains(n)) {
+ continue;
+ }
+ Rect bounds = n.getBounds();
+ gc.fillRect(bounds);
+ }
+ }
+
+ if (state.mBounds != null) {
+ if (state instanceof MoveHandler) {
+ gc.useStyle(DrawingStyle.DROP_PREVIEW);
+ } else {
+ // Resizing
+ if (state.haveSuggestions()) {
+ gc.useStyle(DrawingStyle.RESIZE_PREVIEW);
+ } else {
+ gc.useStyle(DrawingStyle.RESIZE_FAIL);
+ }
+ }
+ gc.drawRect(state.mBounds);
+
+ // Draw baseline preview too
+ if (feedback.dragBaseline != -1) {
+ int y = state.mBounds.y + feedback.dragBaseline;
+ gc.drawLine(state.mBounds.x, y, state.mBounds.x2(), y);
+ }
+ }
+
+ List<String> strings = new ArrayList<String>();
+
+ showMatch(gc, state.mCurrentLeftMatch, state, strings,
+ state.mLeftMargin, ATTR_LAYOUT_MARGIN_LEFT);
+ showMatch(gc, state.mCurrentRightMatch, state, strings,
+ state.mRightMargin, ATTR_LAYOUT_MARGIN_RIGHT);
+ showMatch(gc, state.mCurrentTopMatch, state, strings,
+ state.mTopMargin, ATTR_LAYOUT_MARGIN_TOP);
+ showMatch(gc, state.mCurrentBottomMatch, state, strings,
+ state.mBottomMargin, ATTR_LAYOUT_MARGIN_BOTTOM);
+
+ if (strings.size() > 0) {
+ // Update the drag tooltip
+ StringBuilder sb = new StringBuilder(200);
+ for (String s : strings) {
+ if (sb.length() > 0) {
+ sb.append('\n');
+ }
+ sb.append(s);
+ }
+ feedback.tooltip = sb.toString();
+
+ // Set the tooltip orientation to ensure that it does not interfere with
+ // the constraint arrows
+ if (state.mCurrentLeftMatch != null) {
+ feedback.tooltipX = SegmentType.RIGHT;
+ } else if (state.mCurrentRightMatch != null) {
+ feedback.tooltipX = SegmentType.LEFT;
+ }
+ if (state.mCurrentTopMatch != null) {
+ feedback.tooltipY = SegmentType.BOTTOM;
+ } else if (state.mCurrentBottomMatch != null) {
+ feedback.tooltipY = SegmentType.TOP;
+ }
+ } else {
+ feedback.tooltip = null;
+ }
+
+ if (state.mHorizontalCycle != null) {
+ paintCycle(gc, state, state.mHorizontalCycle);
+ }
+ if (state.mVerticalCycle != null) {
+ paintCycle(gc, state, state.mVerticalCycle);
+ }
+ }
+
+ /** Paints a particular match constraint */
+ private void showMatch(IGraphics gc, Match m, GuidelineHandler state, List<String> strings,
+ int margin, String marginAttribute) {
+ if (m == null) {
+ return;
+ }
+ ConstraintPainter.paintConstraint(gc, state.mBounds, m);
+
+ // Display the constraint. Remove the @id/ and @+id/ prefixes to make the text
+ // shorter and easier to read. This doesn't use stripPrefix() because the id is
+ // usually not a prefix of the value (for example, 'layout_alignBottom=@+id/foo').
+ String constraint = m.getConstraint(false /* generateId */);
+ String description = constraint.replace(NEW_ID_PREFIX, "").replace(ID_PREFIX, "");
+ if (description.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
+ description = description.substring(ATTR_LAYOUT_RESOURCE_PREFIX.length());
+ }
+ if (margin > 0) {
+ int dp = state.getRulesEngine().pxToDp(margin);
+ description = String.format("%1$s, margin=%2$d dp", description, dp);
+ }
+ strings.add(description);
+ }
+
+ /** Paints a constraint cycle */
+ void paintCycle(IGraphics gc, GuidelineHandler state, List<Constraint> cycle) {
+ gc.useStyle(DrawingStyle.CYCLE);
+ assert cycle.size() > 0;
+
+ INode from = cycle.get(0).from.node;
+ Rect fromBounds = from.getBounds();
+ if (state.mDraggedNodes.contains(from)) {
+ fromBounds = state.mBounds;
+ }
+ Point fromCenter = fromBounds.center();
+ INode to = null;
+
+ List<Point> points = new ArrayList<Point>();
+ points.add(fromCenter);
+
+ for (Constraint constraint : cycle) {
+ assert constraint.from.node == from;
+ to = constraint.to.node;
+ assert from != null && to != null;
+
+ Point toCenter = to.getBounds().center();
+ points.add(toCenter);
+
+ // Also go through the dragged node bounds
+ boolean isDragged = state.mDraggedNodes.contains(to);
+ if (isDragged) {
+ toCenter = state.mBounds.center();
+ points.add(toCenter);
+ }
+
+ from = to;
+ fromCenter = toCenter;
+ }
+
+ points.add(fromCenter);
+ points.add(points.get(0));
+
+ for (int i = 1, n = points.size(); i < n; i++) {
+ gc.drawLine(points.get(i-1), points.get(i));
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/Match.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/Match.java
new file mode 100644
index 000000000..6f3f0d0f7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/Match.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.common.layout.relative;
+
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.VALUE_TRUE;
+
+
+import com.android.SdkConstants;
+import static com.android.SdkConstants.ANDROID_URI;
+import com.android.ide.common.api.Segment;
+
+/** A match is a potential pairing of two segments with a given {@link ConstraintType}. */
+class Match {
+ /** the edge of the dragged node that is matched */
+ public final Segment with;
+
+ /** the "other" edge that the dragged edge is matched with */
+ public final Segment edge;
+
+ /** the signed distance between the matched edges */
+ public final int delta;
+
+ /** the type of constraint this is a match for */
+ public final ConstraintType type;
+
+ /** whether this {@link Match} results in a cycle */
+ public boolean cycle;
+
+ /** The associated {@link GuidelineHander} which performed the match */
+ private final GuidelineHandler mHandler;
+
+ /**
+ * Create a new match.
+ *
+ * @param handler the handler which performed the match
+ * @param edge the "other" edge that the dragged edge is matched with
+ * @param with the edge of the dragged node that is matched
+ * @param type the type of constraint this is a match for
+ * @param delta the signed distance between the matched edges
+ */
+ public Match(GuidelineHandler handler, Segment edge, Segment with,
+ ConstraintType type, int delta) {
+ mHandler = handler;
+
+ this.edge = edge;
+ this.with = with;
+ this.type = type;
+ this.delta = delta;
+ }
+
+ /**
+ * Returns the XML constraint attribute value for this match
+ *
+ * @param generateId whether an id should be generated if one is missing
+ * @return the XML constraint attribute value for this match
+ */
+ public String getConstraint(boolean generateId) {
+ if (type.targetParent) {
+ return type.name + '=' + VALUE_TRUE;
+ } else {
+ String id = edge.id;
+ if (id == null || id.length() == -1) {
+ if (!generateId) {
+ // Placeholder to display for the user during dragging
+ id = "<generated>";
+ } else {
+ // Must generate an id on the fly!
+ // See if it's been set by a different constraint we've already applied
+ // to this same node
+ id = edge.node.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id == null || id.length() == 0) {
+ id = mHandler.getRulesEngine().getUniqueId(edge.node.getFqcn());
+ edge.node.setAttribute(ANDROID_URI, ATTR_ID, id);
+ }
+ }
+ }
+ return type.name + '=' + id;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Match [type=" + type + ", delta=" + delta + ", edge=" + edge
+ + "]";
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/MoveHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/MoveHandler.java
new file mode 100644
index 000000000..0fa915d81
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/MoveHandler.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.common.layout.relative;
+
+import static com.android.ide.common.api.MarginType.NO_MARGIN;
+import static com.android.ide.common.api.SegmentType.BASELINE;
+import static com.android.ide.common.api.SegmentType.BOTTOM;
+import static com.android.ide.common.api.SegmentType.CENTER_HORIZONTAL;
+import static com.android.ide.common.api.SegmentType.CENTER_VERTICAL;
+import static com.android.ide.common.api.SegmentType.LEFT;
+import static com.android.ide.common.api.SegmentType.RIGHT;
+import static com.android.ide.common.api.SegmentType.TOP;
+import static com.android.SdkConstants.ATTR_ID;
+
+import static java.lang.Math.abs;
+
+import com.android.SdkConstants;
+import static com.android.SdkConstants.ANDROID_URI;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IClientRulesEngine;
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.Segment;
+import com.android.ide.common.layout.BaseLayoutRule;
+import com.android.ide.common.layout.relative.DependencyGraph.ViewData;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A {@link MoveHandler} is a {@link GuidelineHandler} which handles move and drop
+ * gestures, and offers guideline suggestions and snapping.
+ * <p>
+ * Unlike the {@link ResizeHandler}, the {@link MoveHandler} looks for matches for all
+ * different segment types -- the left edge, the right edge, the baseline, the center
+ * edges, and so on -- and picks the best among these.
+ */
+public class MoveHandler extends GuidelineHandler {
+ private int mDraggedBaseline;
+
+ /**
+ * Creates a new {@link MoveHandler}.
+ *
+ * @param layout the layout element the handler is operating on
+ * @param elements the elements being dragged in the move operation
+ * @param rulesEngine the corresponding {@link IClientRulesEngine}
+ */
+ public MoveHandler(INode layout, IDragElement[] elements, IClientRulesEngine rulesEngine) {
+ super(layout, rulesEngine);
+
+ // Compute list of nodes being dragged within the layout, if any
+ List<INode> nodes = new ArrayList<INode>();
+ for (IDragElement element : elements) {
+ ViewData view = mDependencyGraph.getView(element);
+ if (view != null) {
+ nodes.add(view.node);
+ }
+ }
+ mDraggedNodes = nodes;
+
+ mHorizontalDeps = mDependencyGraph.dependsOn(nodes, false /* verticalEdge */);
+ mVerticalDeps = mDependencyGraph.dependsOn(nodes, true /* verticalEdge */);
+
+ for (INode child : layout.getChildren()) {
+ Rect bc = child.getBounds();
+ if (bc.isValid()) {
+ // First see if this node looks like it's the same as one of the
+ // *dragged* bounds
+ boolean isDragged = false;
+ for (IDragElement element : elements) {
+ // This tries to determine if an INode corresponds to an
+ // IDragElement, by comparing their bounds.
+ if (bc.equals(element.getBounds())) {
+ isDragged = true;
+ }
+ }
+
+ if (!isDragged) {
+ String id = child.getStringAttr(ANDROID_URI, ATTR_ID);
+ // It's okay for id to be null; if you apply a constraint
+ // to a node with a missing id we will generate the id
+
+ boolean addHorizontal = !mHorizontalDeps.contains(child);
+ boolean addVertical = !mVerticalDeps.contains(child);
+
+ addBounds(child, id, addHorizontal, addVertical);
+ if (addHorizontal) {
+ addBaseLine(child, id);
+ }
+ }
+ }
+ }
+
+ String id = layout.getStringAttr(ANDROID_URI, ATTR_ID);
+ addBounds(layout, id, true, true);
+ addCenter(layout, id, true, true);
+ }
+
+ @Override
+ protected void snapVertical(Segment vEdge, int x, Rect newBounds) {
+ int maxDistance = BaseLayoutRule.getMaxMatchDistance();
+ if (vEdge.edgeType == LEFT) {
+ int margin = !mSnap ? 0 : abs(newBounds.x - x);
+ if (margin > maxDistance) {
+ mLeftMargin = margin;
+ } else {
+ newBounds.x = x;
+ }
+ } else if (vEdge.edgeType == RIGHT) {
+ int margin = !mSnap ? 0 : abs(newBounds.x - (x - newBounds.w));
+ if (margin > maxDistance) {
+ mRightMargin = margin;
+ } else {
+ newBounds.x = x - newBounds.w;
+ }
+ } else if (vEdge.edgeType == CENTER_VERTICAL) {
+ newBounds.x = x - newBounds.w / 2;
+ } else {
+ assert false : vEdge;
+ }
+ }
+
+ // TODO: Consider unifying this with the snapping logic in ResizeHandler
+ @Override
+ protected void snapHorizontal(Segment hEdge, int y, Rect newBounds) {
+ int maxDistance = BaseLayoutRule.getMaxMatchDistance();
+ if (hEdge.edgeType == TOP) {
+ int margin = !mSnap ? 0 : abs(newBounds.y - y);
+ if (margin > maxDistance) {
+ mTopMargin = margin;
+ } else {
+ newBounds.y = y;
+ }
+ } else if (hEdge.edgeType == BOTTOM) {
+ int margin = !mSnap ? 0 : abs(newBounds.y - (y - newBounds.h));
+ if (margin > maxDistance) {
+ mBottomMargin = margin;
+ } else {
+ newBounds.y = y - newBounds.h;
+ }
+ } else if (hEdge.edgeType == CENTER_HORIZONTAL) {
+ int margin = !mSnap ? 0 : abs(newBounds.y - (y - newBounds.h / 2));
+ if (margin > maxDistance) {
+ mTopMargin = margin;
+ // or bottomMargin?
+ } else {
+ newBounds.y = y - newBounds.h / 2;
+ }
+ } else if (hEdge.edgeType == BASELINE) {
+ newBounds.y = y - mDraggedBaseline;
+ } else {
+ assert false : hEdge;
+ }
+ }
+
+ /**
+ * Updates the handler for the given mouse move
+ *
+ * @param feedback the feedback handler
+ * @param elements the elements being dragged
+ * @param offsetX the new mouse X coordinate
+ * @param offsetY the new mouse Y coordinate
+ * @param modifierMask the keyboard modifiers pressed during the drag
+ */
+ public void updateMove(DropFeedback feedback, IDragElement[] elements,
+ int offsetX, int offsetY, int modifierMask) {
+ mSnap = (modifierMask & DropFeedback.MODIFIER2) == 0;
+
+ Rect firstBounds = elements[0].getBounds();
+ INode firstNode = null;
+ if (mDraggedNodes != null && mDraggedNodes.size() > 0) {
+ // TODO - this isn't quite right; this could be a different node than we have
+ // bounds for!
+ firstNode = mDraggedNodes.iterator().next();
+ firstBounds = firstNode.getBounds();
+ }
+
+ mBounds = new Rect(offsetX, offsetY, firstBounds.w, firstBounds.h);
+ Rect layoutBounds = layout.getBounds();
+ if (mBounds.x2() > layoutBounds.x2()) {
+ mBounds.x -= mBounds.x2() - layoutBounds.x2();
+ }
+ if (mBounds.y2() > layoutBounds.y2()) {
+ mBounds.y -= mBounds.y2() - layoutBounds.y2();
+ }
+ if (mBounds.x < layoutBounds.x) {
+ mBounds.x = layoutBounds.x;
+ }
+ if (mBounds.y < layoutBounds.y) {
+ mBounds.y = layoutBounds.y;
+ }
+
+ clearSuggestions();
+
+ Rect b = mBounds;
+ Segment edge = new Segment(b.y, b.x, b.x2(), null, null, TOP, NO_MARGIN);
+ List<Match> horizontalMatches = findClosest(edge, mHorizontalEdges);
+ edge = new Segment(b.y2(), b.x, b.x2(), null, null, BOTTOM, NO_MARGIN);
+ addClosest(edge, mHorizontalEdges, horizontalMatches);
+
+ edge = new Segment(b.x, b.y, b.y2(), null, null, LEFT, NO_MARGIN);
+ List<Match> verticalMatches = findClosest(edge, mVerticalEdges);
+ edge = new Segment(b.x2(), b.y, b.y2(), null, null, RIGHT, NO_MARGIN);
+ addClosest(edge, mVerticalEdges, verticalMatches);
+
+ // Match center
+ edge = new Segment(b.centerX(), b.y, b.y2(), null, null, CENTER_VERTICAL, NO_MARGIN);
+ addClosest(edge, mCenterVertEdges, verticalMatches);
+ edge = new Segment(b.centerY(), b.x, b.x2(), null, null, CENTER_HORIZONTAL, NO_MARGIN);
+ addClosest(edge, mCenterHorizEdges, horizontalMatches);
+
+ // Match baseline
+ if (firstNode != null) {
+ int baseline = firstNode.getBaseline();
+ if (baseline != -1) {
+ mDraggedBaseline = baseline;
+ edge = new Segment(b.y + baseline, b.x, b.x2(), firstNode, null, BASELINE,
+ NO_MARGIN);
+ addClosest(edge, mHorizontalEdges, horizontalMatches);
+ }
+ } else {
+ int baseline = feedback.dragBaseline;
+ if (baseline != -1) {
+ mDraggedBaseline = baseline;
+ edge = new Segment(offsetY + baseline, b.x, b.x2(), null, null, BASELINE,
+ NO_MARGIN);
+ addClosest(edge, mHorizontalEdges, horizontalMatches);
+ }
+ }
+
+ mHorizontalSuggestions = horizontalMatches;
+ mVerticalSuggestions = verticalMatches;
+ mTopMargin = mBottomMargin = mLeftMargin = mRightMargin = 0;
+
+ Match match = pickBestMatch(mHorizontalSuggestions);
+ if (match != null) {
+ if (mHorizontalDeps.contains(match.edge.node)) {
+ match.cycle = true;
+ }
+
+ // Reset top AND bottom bounds regardless of whether both are bound
+ mMoveTop = true;
+ mMoveBottom = true;
+
+ // TODO: Consider doing the snap logic on all the possible matches
+ // BEFORE sorting, in case this affects the best-pick algorithm (since some
+ // edges snap and others don't).
+ snapHorizontal(match.with, match.edge.at, mBounds);
+
+ if (match.with.edgeType == TOP) {
+ mCurrentTopMatch = match;
+ } else if (match.with.edgeType == BOTTOM) {
+ mCurrentBottomMatch = match;
+ } else {
+ assert match.with.edgeType == CENTER_HORIZONTAL
+ || match.with.edgeType == BASELINE : match.with.edgeType;
+ mCurrentTopMatch = match;
+ }
+ }
+
+ match = pickBestMatch(mVerticalSuggestions);
+ if (match != null) {
+ if (mVerticalDeps.contains(match.edge.node)) {
+ match.cycle = true;
+ }
+
+ // Reset left AND right bounds regardless of whether both are bound
+ mMoveLeft = true;
+ mMoveRight = true;
+
+ snapVertical(match.with, match.edge.at, mBounds);
+
+ if (match.with.edgeType == LEFT) {
+ mCurrentLeftMatch = match;
+ } else if (match.with.edgeType == RIGHT) {
+ mCurrentRightMatch = match;
+ } else {
+ assert match.with.edgeType == CENTER_VERTICAL;
+ mCurrentLeftMatch = match;
+ }
+ }
+
+ checkCycles(feedback);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ResizeHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ResizeHandler.java
new file mode 100644
index 000000000..a5e071d74
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/ResizeHandler.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.common.layout.relative;
+
+import static com.android.ide.common.api.MarginType.NO_MARGIN;
+import static com.android.ide.common.api.SegmentType.BASELINE;
+import static com.android.ide.common.api.SegmentType.BOTTOM;
+import static com.android.ide.common.api.SegmentType.CENTER_HORIZONTAL;
+import static com.android.ide.common.api.SegmentType.CENTER_VERTICAL;
+import static com.android.ide.common.api.SegmentType.LEFT;
+import static com.android.ide.common.api.SegmentType.RIGHT;
+import static com.android.ide.common.api.SegmentType.TOP;
+import static com.android.SdkConstants.ATTR_ID;
+
+import static java.lang.Math.abs;
+
+import com.android.SdkConstants;
+import static com.android.SdkConstants.ANDROID_URI;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IClientRulesEngine;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.Segment;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.common.layout.BaseLayoutRule;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * A {@link ResizeHandler} is a {@link GuidelineHandler} which handles resizing of individual
+ * edges in a RelativeLayout.
+ */
+public class ResizeHandler extends GuidelineHandler {
+ private final SegmentType mHorizontalEdgeType;
+ private final SegmentType mVerticalEdgeType;
+
+ /**
+ * Creates a new {@link ResizeHandler}
+ *
+ * @param layout the layout containing the resized node
+ * @param resized the node being resized
+ * @param rulesEngine the applicable {@link IClientRulesEngine}
+ * @param horizontalEdgeType the type of horizontal edge being resized, or null
+ * @param verticalEdgeType the type of vertical edge being resized, or null
+ */
+ public ResizeHandler(INode layout, INode resized,
+ IClientRulesEngine rulesEngine,
+ SegmentType horizontalEdgeType, SegmentType verticalEdgeType) {
+ super(layout, rulesEngine);
+
+ assert horizontalEdgeType != null || verticalEdgeType != null;
+ assert horizontalEdgeType != BASELINE && verticalEdgeType != BASELINE;
+ assert horizontalEdgeType != CENTER_HORIZONTAL && verticalEdgeType != CENTER_HORIZONTAL;
+ assert horizontalEdgeType != CENTER_VERTICAL && verticalEdgeType != CENTER_VERTICAL;
+
+ mHorizontalEdgeType = horizontalEdgeType;
+ mVerticalEdgeType = verticalEdgeType;
+
+ Set<INode> nodes = Collections.singleton(resized);
+ mDraggedNodes = nodes;
+
+ mHorizontalDeps = mDependencyGraph.dependsOn(nodes, false /* vertical */);
+ mVerticalDeps = mDependencyGraph.dependsOn(nodes, true /* vertical */);
+
+ if (horizontalEdgeType != null) {
+ if (horizontalEdgeType == TOP) {
+ mMoveTop = true;
+ } else if (horizontalEdgeType == BOTTOM) {
+ mMoveBottom = true;
+ }
+ }
+ if (verticalEdgeType != null) {
+ if (verticalEdgeType == LEFT) {
+ mMoveLeft = true;
+ } else if (verticalEdgeType == RIGHT) {
+ mMoveRight = true;
+ }
+ }
+
+ for (INode child : layout.getChildren()) {
+ if (child != resized) {
+ String id = child.getStringAttr(ANDROID_URI, ATTR_ID);
+ addBounds(child, id,
+ !mHorizontalDeps.contains(child),
+ !mVerticalDeps.contains(child));
+ }
+ }
+
+ addBounds(layout, layout.getStringAttr(ANDROID_URI, ATTR_ID), true, true);
+ }
+
+ @Override
+ protected void snapVertical(Segment vEdge, int x, Rect newBounds) {
+ int maxDistance = BaseLayoutRule.getMaxMatchDistance();
+ if (vEdge.edgeType == LEFT) {
+ int margin = mSnap ? 0 : abs(newBounds.x - x);
+ if (margin > maxDistance) {
+ mLeftMargin = margin;
+ } else {
+ newBounds.w += newBounds.x - x;
+ newBounds.x = x;
+ }
+ } else if (vEdge.edgeType == RIGHT) {
+ int margin = mSnap ? 0 : abs(newBounds.x - (x - newBounds.w));
+ if (margin > maxDistance) {
+ mRightMargin = margin;
+ } else {
+ newBounds.w = x - newBounds.x;
+ }
+ } else {
+ assert false : vEdge;
+ }
+ }
+
+ @Override
+ protected void snapHorizontal(Segment hEdge, int y, Rect newBounds) {
+ int maxDistance = BaseLayoutRule.getMaxMatchDistance();
+ if (hEdge.edgeType == TOP) {
+ int margin = mSnap ? 0 : abs(newBounds.y - y);
+ if (margin > maxDistance) {
+ mTopMargin = margin;
+ } else {
+ newBounds.h += newBounds.y - y;
+ newBounds.y = y;
+ }
+ } else if (hEdge.edgeType == BOTTOM) {
+ int margin = mSnap ? 0 : abs(newBounds.y - (y - newBounds.h));
+ if (margin > maxDistance) {
+ mBottomMargin = margin;
+ } else {
+ newBounds.h = y - newBounds.y;
+ }
+ } else {
+ assert false : hEdge;
+ }
+ }
+
+ @Override
+ protected boolean isEdgeTypeCompatible(SegmentType edge, SegmentType dragged, int delta) {
+ boolean compatible = super.isEdgeTypeCompatible(edge, dragged, delta);
+
+ // When resizing and not snapping (e.g. using margins to pick a specific pixel
+ // width) we cannot use -negative- margins to jump back to a closer edge; we
+ // must always use positive margins, so mark closer edges that result in a negative
+ // margin as not compatible.
+ if (compatible && !mSnap) {
+ switch (dragged) {
+ case LEFT:
+ case TOP:
+ return delta <= 0;
+ default:
+ return delta >= 0;
+ }
+ }
+
+ return compatible;
+ }
+
+ /**
+ * Updates the handler for the given mouse resize
+ *
+ * @param feedback the feedback handler
+ * @param child the node being resized
+ * @param newBounds the new bounds of the resize rectangle
+ * @param modifierMask the keyboard modifiers pressed during the drag
+ */
+ public void updateResize(DropFeedback feedback, INode child, Rect newBounds,
+ int modifierMask) {
+ mSnap = (modifierMask & DropFeedback.MODIFIER2) == 0;
+ mBounds = newBounds;
+ clearSuggestions();
+
+ Rect b = newBounds;
+ Segment hEdge = null;
+ Segment vEdge = null;
+ String childId = child.getStringAttr(ANDROID_URI, ATTR_ID);
+
+ // TODO: MarginType=NO_MARGIN may not be right. Consider resizing a widget
+ // that has margins and how that should be handled.
+
+ if (mHorizontalEdgeType == TOP) {
+ hEdge = new Segment(b.y, b.x, b.x2(), child, childId, mHorizontalEdgeType, NO_MARGIN);
+ } else if (mHorizontalEdgeType == BOTTOM) {
+ hEdge = new Segment(b.y2(), b.x, b.x2(), child, childId, mHorizontalEdgeType,
+ NO_MARGIN);
+ } else {
+ assert mHorizontalEdgeType == null;
+ }
+
+ if (mVerticalEdgeType == LEFT) {
+ vEdge = new Segment(b.x, b.y, b.y2(), child, childId, mVerticalEdgeType, NO_MARGIN);
+ } else if (mVerticalEdgeType == RIGHT) {
+ vEdge = new Segment(b.x2(), b.y, b.y2(), child, childId, mVerticalEdgeType, NO_MARGIN);
+ } else {
+ assert mVerticalEdgeType == null;
+ }
+
+ mTopMargin = mBottomMargin = mLeftMargin = mRightMargin = 0;
+
+ if (hEdge != null && mHorizontalEdges.size() > 0) {
+ // Compute horizontal matches
+ mHorizontalSuggestions = findClosest(hEdge, mHorizontalEdges);
+
+ Match match = pickBestMatch(mHorizontalSuggestions);
+ if (match != null
+ && (!mSnap || Math.abs(match.delta) < BaseLayoutRule.getMaxMatchDistance())) {
+ if (mHorizontalDeps.contains(match.edge.node)) {
+ match.cycle = true;
+ }
+
+ snapHorizontal(hEdge, match.edge.at, newBounds);
+
+ if (hEdge.edgeType == TOP) {
+ mCurrentTopMatch = match;
+ } else if (hEdge.edgeType == BOTTOM) {
+ mCurrentBottomMatch = match;
+ } else {
+ assert hEdge.edgeType == CENTER_HORIZONTAL
+ || hEdge.edgeType == BASELINE : hEdge;
+ mCurrentTopMatch = match;
+ }
+ }
+ }
+
+ if (vEdge != null && mVerticalEdges.size() > 0) {
+ mVerticalSuggestions = findClosest(vEdge, mVerticalEdges);
+
+ Match match = pickBestMatch(mVerticalSuggestions);
+ if (match != null
+ && (!mSnap || Math.abs(match.delta) < BaseLayoutRule.getMaxMatchDistance())) {
+ if (mVerticalDeps.contains(match.edge.node)) {
+ match.cycle = true;
+ }
+
+ // Snap
+ snapVertical(vEdge, match.edge.at, newBounds);
+
+ if (vEdge.edgeType == LEFT) {
+ mCurrentLeftMatch = match;
+ } else if (vEdge.edgeType == RIGHT) {
+ mCurrentRightMatch = match;
+ } else {
+ assert vEdge.edgeType == CENTER_VERTICAL;
+ mCurrentLeftMatch = match;
+ }
+ }
+ }
+
+ checkCycles(feedback);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/removecol.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/removecol.png
new file mode 100644
index 000000000..c41261afa
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/removecol.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/removerow.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/removerow.png
new file mode 100644
index 000000000..db695a714
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/removerow.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/showgrid.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/showgrid.png
new file mode 100644
index 000000000..6f7bf9160
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/showgrid.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/snap.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/snap.png
new file mode 100644
index 000000000..b50a16ed1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/snap.png
Binary files differ
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 000000000..e5d753885
--- /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/common/layout/vlinear.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/vlinear.png
new file mode 100644
index 000000000..e03c16e00
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/vlinear.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/weights.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/weights.png
new file mode 100644
index 000000000..cb654a140
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/weights.png
Binary files differ