OSDN Git Service

original
[gb-231r1-is01/GB_2.3_IS01.git] / sdk / eclipse / plugins / com.android.ide.eclipse.adt / src / com / android / ide / common / layout / RelativeLayoutRule.java
diff --git a/sdk/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java b/sdk/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java
new file mode 100644 (file)
index 0000000..33fe202
--- /dev/null
@@ -0,0 +1,796 @@
+/*
+ * 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.ide.common.layout.LayoutConstants.ANDROID_URI;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_GRAVITY;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_ABOVE;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_BASELINE;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_BOTTOM;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_LEFT;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_PARENT_BOTTOM;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_PARENT_LEFT;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_PARENT_RIGHT;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_PARENT_TOP;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_RIGHT;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_TOP;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_ALIGN_WITH_PARENT_MISSING;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_BELOW;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_CENTER_HORIZONTAL;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_CENTER_IN_PARENT;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_CENTER_VERTICAL;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_TO_LEFT_OF;
+import static com.android.ide.common.layout.LayoutConstants.VAUE_TO_RIGHT_OF;
+
+import com.android.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.IFeedbackPainter;
+import com.android.ide.common.api.IGraphics;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.INodeHandler;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.MenuAction;
+import com.android.ide.common.api.Point;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.api.INode.IAttribute;
+import com.android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An {@link IViewRule} for android.widget.RelativeLayout and all its derived
+ * classes.
+ */
+public class RelativeLayoutRule extends BaseLayoutRule {
+
+    // ==== Selection ====
+
+    @Override
+    public List<String> getSelectionHint(INode parentNode, INode childNode) {
+        List<String> infos = new ArrayList<String>(18);
+        addAttr(VALUE_ABOVE, childNode, infos);
+        addAttr(VALUE_BELOW, childNode, infos);
+        addAttr(VALUE_TO_LEFT_OF, childNode, infos);
+        addAttr(VAUE_TO_RIGHT_OF, childNode, infos);
+        addAttr(VALUE_ALIGN_BASELINE, childNode, infos);
+        addAttr(VALUE_ALIGN_TOP, childNode, infos);
+        addAttr(VALUE_ALIGN_BOTTOM, childNode, infos);
+        addAttr(VALUE_ALIGN_LEFT, childNode, infos);
+        addAttr(VALUE_ALIGN_RIGHT, childNode, infos);
+        addAttr(VALUE_ALIGN_PARENT_TOP, childNode, infos);
+        addAttr(VALUE_ALIGN_PARENT_BOTTOM, childNode, infos);
+        addAttr(VALUE_ALIGN_PARENT_LEFT, childNode, infos);
+        addAttr(VALUE_ALIGN_PARENT_RIGHT, childNode, infos);
+        addAttr(VALUE_ALIGN_WITH_PARENT_MISSING, childNode, infos);
+        addAttr(VALUE_CENTER_HORIZONTAL, childNode, infos);
+        addAttr(VALUE_CENTER_IN_PARENT, childNode, infos);
+        addAttr(VALUE_CENTER_VERTICAL, childNode, infos);
+
+        return infos;
+    }
+
+    private void addAttr(String propertyName, INode childNode, List<String> infos) {
+        String a = childNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_PREFIX + propertyName);
+        if (a != null && a.length() > 0) {
+            String s = propertyName + ": " + a;
+            infos.add(s);
+        }
+    }
+
+    // ==== Drag'n'drop support ====
+
+    @Override
+    public DropFeedback onDropEnter(INode targetNode, final IDragElement[] elements) {
+
+        if (elements.length == 0) {
+            return null;
+        }
+
+        Rect bn = targetNode.getBounds();
+        if (!bn.isValid()) {
+            return null;
+        }
+
+        // Collect the ids of the elements being dragged
+        List<String> movedIds = new ArrayList<String>(collectIds(
+                new HashMap<String, Pair<String, String>>(), elements).keySet());
+
+        // Prepare the drop feedback
+        return new DropFeedback(new RelativeDropData(movedIds), new IFeedbackPainter() {
+            public void paint(IGraphics gc, INode node, DropFeedback feedback) {
+                drawRelativeDropFeedback(gc, node, elements, feedback);
+            }
+        });
+    }
+
+    @Override
+    public DropFeedback onDropMove(INode targetNode, IDragElement[] elements,
+            DropFeedback feedback, Point p) {
+
+        RelativeDropData data = (RelativeDropData) feedback.userData;
+        Rect area = feedback.captureArea;
+
+        // Only look for a new child if cursor is no longer under the current
+        // rect
+        if (area == null || !area.contains(p)) {
+
+            // We're not capturing anymore since we got outside of the capture
+            // bounds
+            feedback.captureArea = null;
+            feedback.requestPaint = false;
+            data.setRejected(null);
+
+            // Find the current direct children under the cursor
+            INode childNode = null;
+            int childIndex = -1;
+            nextChild: for (INode child : targetNode.getChildren()) {
+                childIndex++;
+                Rect bc = child.getBounds();
+                if (bc.contains(p)) {
+
+                    // If we're doing a move operation within the same canvas,
+                    // we can't attach the moved object to one belonging to the
+                    // selection since it will disappear after the move.
+                    if (feedback.sameCanvas && !feedback.isCopy) {
+                        for (IDragElement element : elements) {
+                            if (bc.equals(element.getBounds())) {
+                                data.setRejected(bc);
+                                feedback.requestPaint = true;
+                                continue nextChild;
+                            }
+                        }
+                    }
+
+                    // One more limitation: if we're moving one or more
+                    // elements, we can't drop them on a child which relative
+                    // position is expressed directly or indirectly based on the
+                    // element being moved.
+                    if (!feedback.isCopy) {
+                        if (searchRelativeIds(child, data.getMovedIds(),
+                                data.getCachedLinkIds())) {
+                            data.setRejected(bc);
+                            feedback.requestPaint = true;
+                            continue nextChild;
+                        }
+                    }
+
+                    childNode = child;
+                    break;
+                }
+            }
+
+            // If there is a selected child and it changed, recompute child drop
+            // zones
+            if (childNode != null && childNode != data.getChild()) {
+                data.setChild(childNode);
+                data.setIndex(childIndex);
+                data.setCurr(null);
+                data.setZones(null);
+
+                Pair<Rect, List<DropZone>> result = computeChildDropZones(childNode);
+                data.setZones(result.getSecond());
+
+                // Capture this rect, to prevent the engine from switching the
+                // layout node.
+                feedback.captureArea = result.getFirst();
+                feedback.requestPaint = true;
+
+            } else if (childNode == null) {
+                // If there is no selected child, compute the border drop zone
+                data.setChild(null);
+                data.setIndex(-1);
+                data.setCurr(null);
+
+                DropZone zone = computeBorderDropZone(targetNode, p, feedback);
+                if (zone == null) {
+                    data.setZones(null);
+                } else {
+                    data.setZones(Collections.singletonList(zone));
+                    feedback.captureArea = zone.getRect();
+                }
+
+                feedback.requestPaint |= (area == null || !area.equals(feedback.captureArea));
+            }
+        }
+
+        // Find the current zone
+        DropZone currZone = null;
+        if (data.getZones() != null) {
+            for (DropZone zone : data.getZones()) {
+                if (zone.getRect().contains(p)) {
+                    currZone = zone;
+                    break;
+                }
+            }
+
+            // Look to see if there's a border match if we didn't find anything better;
+            // a border match isn't required to have the mouse cursor within it since we
+            // do edge matching in the code which -adds- the border zones.
+            if (currZone == null && feedback.dragBounds != null) {
+                for (DropZone zone : data.getZones()) {
+                    if (zone.isBorderZone()) {
+                        currZone = zone;
+                        break;
+                    }
+                }
+            }
+        }
+
+        // Look for border match when there are no children: always offer one in this case
+        if (currZone == null && targetNode.getChildren().length == 0 && data.getZones() != null
+                && data.getZones().size() > 0) {
+            currZone = data.getZones().get(0);
+        }
+
+        if (currZone != data.getCurr()) {
+            data.setCurr(currZone);
+            feedback.requestPaint = true;
+        }
+
+        feedback.invalidTarget = (currZone == null);
+
+        return feedback;
+    }
+
+    /**
+     * Returns true if the child has any attribute of Format.REFERENCE which
+     * value matches one of the ids in movedIds.
+     */
+    private boolean searchRelativeIds(INode node, List<String> movedIds,
+            Map<INode, Set<String>> cachedLinkIds) {
+        Set<String> ids = getLinkedIds(node, cachedLinkIds);
+
+        for (String id : ids) {
+            if (movedIds.contains(id)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private Set<String> getLinkedIds(INode node, Map<INode, Set<String>> cachedLinkIds) {
+        Set<String> ids = cachedLinkIds.get(node);
+
+        if (ids != null) {
+            return ids;
+        }
+
+        // We don't have cached data on this child, so create a list of
+        // all the linked id it is referencing.
+        ids = new HashSet<String>();
+        cachedLinkIds.put(node, ids);
+        for (IAttribute attr : node.getLiveAttributes()) {
+            IAttributeInfo attrInfo = node.getAttributeInfo(attr.getUri(), attr.getName());
+            if (attrInfo == null) {
+                continue;
+            }
+            Format[] formats = attrInfo.getFormats();
+            if (!IAttributeInfo.Format.REFERENCE.in(formats)) {
+                continue;
+            }
+
+            String id = attr.getValue();
+            id = normalizeId(id);
+            if (ids.contains(id)) {
+                continue;
+            }
+            ids.add(id);
+
+            // Find the sibling with that id
+            INode p = node.getParent();
+            if (p == null) {
+                continue;
+            }
+            for (INode child : p.getChildren()) {
+                if (child == node) {
+                    continue;
+                }
+                String childId = child.getStringAttr(ANDROID_URI, ATTR_ID);
+                if (childId == null) {
+                    continue;
+                }
+                childId = normalizeId(childId);
+                if (id.equals(childId)) {
+                    Set<String> linkedIds = getLinkedIds(child, cachedLinkIds);
+                    ids.addAll(linkedIds);
+                    break;
+                }
+            }
+        }
+
+        return ids;
+    }
+
+    private DropZone computeBorderDropZone(INode targetNode, Point p, DropFeedback feedback) {
+        Rect bounds = targetNode.getBounds();
+        int x = p.x;
+        int y = p.y;
+
+        int x1 = bounds.x;
+        int y1 = bounds.y;
+        int w = bounds.w;
+        int h = bounds.h;
+        int x2 = x1 + w;
+        int y2 = y1 + h;
+
+        // Default border zone size
+        int n = 10;
+        int n2 = 2*n;
+
+        // Size of -matched- border zone (not painted, but we detect edge overlaps here)
+        int hn = 0;
+        int vn = 0;
+        if (feedback.dragBounds != null) {
+            hn = feedback.dragBounds.w / 2;
+            vn = feedback.dragBounds.h / 2;
+        }
+        boolean vertical = false;
+
+        Rect r = null;
+        String attr = null;
+
+        if (x <= x1 + n + hn && y >= y1 && y <= y2) {
+            r = new Rect(x1 - n, y1, n2, h);
+            attr = VALUE_ALIGN_PARENT_LEFT;
+            vertical = true;
+
+        } else if (x >= x2 - hn - n && y >= y1 && y <= y2) {
+            r = new Rect(x2 - n, y1, n2, h);
+            attr = VALUE_ALIGN_PARENT_RIGHT;
+            vertical = true;
+
+        } else if (y <= y1 + n + vn && x >= x1 && x <= x2) {
+            r = new Rect(x1, y1 - n, w, n2);
+            attr = VALUE_ALIGN_PARENT_TOP;
+
+        } else if (y >= y2 - vn - n && x >= x1 && x <= x2) {
+            r = new Rect(x1, y2 - n, w, n2);
+            attr = VALUE_ALIGN_PARENT_BOTTOM;
+
+        } else {
+            // We're nowhere near a border.
+            // If there are no children, we will offer one anyway:
+            if (targetNode.getChildren().length == 0) {
+                r = new Rect(x1 - n, y1, n2, h);
+                attr = VALUE_ALIGN_PARENT_LEFT;
+                vertical = true;
+            } else {
+                return null;
+            }
+        }
+
+        return new DropZone(r, Collections.singletonList(attr), r.getCenter(), vertical);
+    }
+
+    private Pair<Rect, List<DropZone>> computeChildDropZones(INode childNode) {
+
+        Rect b = childNode.getBounds();
+
+        // Compute drop zone borders as follow:
+        //
+        // +---+-----+-----+-----+---+
+        // | 1 \ 2 \ 3 / 4 / 5 |
+        // +----+-----+---+-----+----+
+        //
+        // For the top and bottom borders, zones 1 and 5 have the same width,
+        // which is
+        // size1 = min(10, w/5)
+        // and zones 2, 3 and 4 have a width of
+        // size2 = (w - 2*size) / 3
+        //
+        // Same works for left and right borders vertically.
+        //
+        // Attributes generated:
+        // Horizontally:
+        // 1- toLeftOf / 2- alignLeft / 3- 2+4 / 4- alignRight / 5- toRightOf
+        // Vertically:
+        // 1- above / 2-alignTop / 3- 2+4 / 4- alignBottom / 5- below
+
+        int w1 = 20;
+        int w3 = b.w / 3;
+        int w2 = Math.max(20, w3);
+
+        int h1 = 20;
+        int h3 = b.h / 3;
+        int h2 = Math.max(20, h3);
+
+        int wt = w1 * 2 + w2 * 3;
+        int ht = h1 * 2 + h2 * 3;
+
+        int x1 = b.x + ((b.w - wt) / 2);
+        int y1 = b.y + ((b.h - ht) / 2);
+
+        Rect bounds = new Rect(x1, y1, wt, ht);
+
+        List<DropZone> zones = new ArrayList<DropZone>(16);
+        String a = VALUE_ABOVE;
+        int x = x1;
+        int y = y1;
+
+        x = addx(w1, a, x, y, h1, zones, VALUE_TO_LEFT_OF);
+        x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_LEFT);
+        x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_LEFT, VALUE_ALIGN_RIGHT);
+        x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_RIGHT);
+        x = addx(w1, a, x, y, h1, zones, VAUE_TO_RIGHT_OF);
+
+        a = VALUE_BELOW;
+        x = x1;
+        y = y1 + ht - h1;
+
+        x = addx(w1, a, x, y, h1, zones, VALUE_TO_LEFT_OF);
+        x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_LEFT);
+        x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_LEFT, VALUE_ALIGN_RIGHT);
+        x = addx(w2, a, x, y, h1, zones, VALUE_ALIGN_RIGHT);
+        x = addx(w1, a, x, y, h1, zones, VAUE_TO_RIGHT_OF);
+
+        a = VALUE_TO_LEFT_OF;
+        x = x1;
+        y = y1 + h1;
+
+        y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_TOP);
+        y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_TOP, VALUE_ALIGN_BOTTOM);
+        y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_BOTTOM);
+
+        a = VAUE_TO_RIGHT_OF;
+        x = x1 + wt - w1;
+        y = y1 + h1;
+
+        y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_TOP);
+        y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_TOP, VALUE_ALIGN_BOTTOM);
+        y = addy(h2, a, x, y, w1, zones, VALUE_ALIGN_BOTTOM);
+
+        return Pair.of(bounds, zones);
+    }
+
+    private int addx(int wn, String a, int x, int y, int h1, List<DropZone> zones, String... a2) {
+        Rect rect = new Rect(x, y, wn, h1);
+        List<String> attrs = new ArrayList<String>(a2.length + 1);
+        attrs.add(a);
+        for (String attribute : a2) {
+            attrs.add(attribute);
+        }
+        zones.add(new DropZone(rect, attrs));
+        return x + wn;
+    }
+
+    private int addy(int hn, String a, int x, int y, int w1, List<DropZone> zones, String... a2) {
+        Rect rect = new Rect(x, y, w1, hn);
+        List<String> attrs = new ArrayList<String>(a2.length + 1);
+        attrs.add(a);
+        for (String attribute : a2) {
+            attrs.add(attribute);
+        }
+
+        zones.add(new DropZone(rect, attrs));
+        return y + hn;
+    }
+
+    private void drawRelativeDropFeedback(IGraphics gc, INode targetNode, IDragElement[] elements,
+            DropFeedback feedback) {
+        Rect b = targetNode.getBounds();
+        if (!b.isValid()) {
+            return;
+        }
+
+        gc.useStyle(DrawingStyle.DROP_RECIPIENT);
+        gc.drawRect(b);
+
+        gc.useStyle(DrawingStyle.DROP_ZONE);
+
+        RelativeDropData data = (RelativeDropData) feedback.userData;
+
+        if (data.getZones() != null) {
+            for (DropZone it : data.getZones()) {
+                gc.drawRect(it.getRect());
+            }
+        }
+
+        if (data.getCurr() != null) {
+            gc.useStyle(DrawingStyle.DROP_ZONE_ACTIVE);
+            gc.fillRect(data.getCurr().getRect());
+
+            Rect r = feedback.captureArea;
+            int x = r.x + 5;
+            int y = r.y + r.h + 5;
+
+            String id = null;
+            if (data.getChild() != null) {
+                id = data.getChild().getStringAttr(ANDROID_URI, ATTR_ID);
+            }
+
+            // Print constraints (with id appended if applicable)
+            gc.useStyle(DrawingStyle.HELP);
+            List<String> strings = new ArrayList<String>();
+            for (String it : data.getCurr().getAttr()) {
+                strings.add(id != null && id.length() > 0 ? it + "=" + id : it);
+            }
+            gc.drawBoxedStrings(x, y, strings);
+
+            Point mark = data.getCurr().getMark();
+            if (mark != null) {
+                gc.useStyle(DrawingStyle.DROP_PREVIEW);
+                Rect nr = data.getCurr().getRect();
+                int nx = nr.x + nr.w / 2;
+                int ny = nr.y + nr.h / 2;
+                boolean vertical = data.getCurr().isVertical();
+                if (vertical) {
+                    gc.drawLine(nx, nr.y, nx, nr.y + nr.h);
+                    x = nx;
+                    y = b.y;
+                } else {
+                    gc.drawLine(nr.x, ny, nr.x + nr.w, ny);
+                    x = b.x;
+                    y = ny;
+                }
+            } else {
+                r = data.getCurr().getRect();
+                x = r.x + r.w / 2;
+                y = r.y + r.h / 2;
+            }
+
+            Rect be = elements[0].getBounds();
+
+            // Draw bound rectangles for all selected items
+            gc.useStyle(DrawingStyle.DROP_PREVIEW);
+            for (IDragElement element : elements) {
+                be = element.getBounds();
+                if (!be.isValid()) {
+                    // We don't always have bounds - for example when dragging
+                    // from the palette.
+                    continue;
+                }
+
+                int offsetX = x - be.x;
+                int offsetY = y - be.y;
+
+                if (data.getCurr().getAttr().contains(VALUE_ALIGN_TOP)
+                        && data.getCurr().getAttr().contains(VALUE_ALIGN_BOTTOM)) {
+                    offsetY -= be.h / 2;
+                } else if (data.getCurr().getAttr().contains(VALUE_ABOVE)
+                        || data.getCurr().getAttr().contains(VALUE_ALIGN_TOP)
+                        || data.getCurr().getAttr().contains(VALUE_ALIGN_PARENT_BOTTOM)) {
+                    offsetY -= be.h;
+                }
+                if (data.getCurr().getAttr().contains(VALUE_ALIGN_RIGHT)
+                        && data.getCurr().getAttr().contains(VALUE_ALIGN_LEFT)) {
+                    offsetX -= be.w / 2;
+                } else if (data.getCurr().getAttr().contains(VALUE_TO_LEFT_OF)
+                        || data.getCurr().getAttr().contains(VALUE_ALIGN_LEFT)
+                        || data.getCurr().getAttr().contains(VALUE_ALIGN_PARENT_RIGHT)) {
+                    offsetX -= be.w;
+                }
+
+                drawElement(gc, element, offsetX, offsetY);
+            }
+        }
+
+        if (data.getRejected() != null) {
+            Rect br = data.getRejected();
+            gc.useStyle(DrawingStyle.INVALID);
+            gc.fillRect(br);
+            gc.drawLine(br.x, br.y, br.x + br.w, br.y + br.h);
+            gc.drawLine(br.x, br.y + br.h, br.x + br.w, br.y);
+        }
+    }
+
+    @Override
+    public void onDropLeave(INode targetNode, IDragElement[] elements, DropFeedback feedback) {
+        // Free the last captured rect, if any
+        feedback.captureArea = null;
+    }
+
+    @Override
+    public void onDropped(final INode targetNode, final IDragElement[] elements,
+            final DropFeedback feedback, final Point p) {
+        final RelativeDropData data = (RelativeDropData) feedback.userData;
+        if (data.getCurr() == null) {
+            return;
+        }
+
+        // Collect IDs from dropped elements and remap them to new IDs
+        // if this is a copy or from a different canvas.
+        final Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements,
+                feedback.isCopy || !feedback.sameCanvas);
+
+        targetNode.editXml("Add elements to RelativeLayout", new INodeHandler() {
+
+            public void handle(INode node) {
+                int index = data.getIndex();
+
+                // Now write the new elements.
+                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, DEFAULT_ATTR_FILTER);
+
+                    // TODO... seems totally wrong. REVISIT or EXPLAIN
+                    String id = null;
+                    if (data.getChild() != null) {
+                        id = data.getChild().getStringAttr(ANDROID_URI, ATTR_ID);
+                    }
+
+                    for (String it : data.getCurr().getAttr()) {
+                        newChild.setAttribute(ANDROID_URI,
+                             ATTR_LAYOUT_PREFIX + it, id != null ? id : "true"); //$NON-NLS-1$
+                    }
+
+                    addInnerElements(newChild, element, idMap);
+                }
+            }
+        });
+    }
+
+    @Override
+    public void addLayoutActions(List<MenuAction> actions, final INode parentNode,
+            final List<? extends INode> children) {
+        super.addLayoutActions(actions, parentNode, children);
+
+        actions.add(createGravityAction(Collections.<INode>singletonList(parentNode),
+                ATTR_GRAVITY));
+        actions.add(MenuAction.createSeparator(25));
+        actions.add(createMarginAction(parentNode, children));
+    }
+
+    /**
+     * Internal state used by the RelativeLayoutRule, stored as userData in the
+     * {@link DropFeedback}.
+     */
+    private static class RelativeDropData {
+        /** Current child under cursor */
+        private INode mChild;
+
+        /** Index of child in the parent children list */
+        private int mIndex;
+
+        /**
+         * Valid "anchor" zones for the current child of type
+         */
+        private List<DropZone> mZones;
+
+        /** Current zone */
+        private DropZone mCurr;
+
+        /** rejected target (Rect bounds) */
+        private Rect mRejected;
+
+        private List<String> mMovedIds;
+
+        private Map<INode, Set<String>> mCachedLinkIds = new HashMap<INode, Set<String>>();
+
+        public RelativeDropData(List<String> movedIds) {
+            this.mMovedIds = movedIds;
+        }
+
+        private void setChild(INode child) {
+            this.mChild = child;
+        }
+
+        private INode getChild() {
+            return mChild;
+        }
+
+        private void setIndex(int index) {
+            this.mIndex = index;
+        }
+
+        private int getIndex() {
+            return mIndex;
+        }
+
+        private void setZones(List<DropZone> zones) {
+            this.mZones = zones;
+        }
+
+        private List<DropZone> getZones() {
+            return mZones;
+        }
+
+        private void setCurr(DropZone curr) {
+            this.mCurr = curr;
+        }
+
+        private DropZone getCurr() {
+            return mCurr;
+        }
+
+        private void setRejected(Rect rejected) {
+            this.mRejected = rejected;
+        }
+
+        private Rect getRejected() {
+            return mRejected;
+        }
+
+        private List<String> getMovedIds() {
+            return mMovedIds;
+        }
+
+        private Map<INode, Set<String>> getCachedLinkIds() {
+            return mCachedLinkIds;
+        }
+    }
+
+    private static class DropZone {
+        /** The rectangular bounds of the drop zone */
+        private final Rect mRect;
+
+        /**
+         * Attributes that correspond to this drop zone, e.g. ["alignLeft",
+         * "alignBottom"]
+         */
+        private final List<String> mAttr;
+
+        /** Non-null iff this is a border */
+        private final Point mMark;
+
+        /** Defined iff this is a border match */
+        private final boolean mVertical;
+
+        public DropZone(Rect rect, List<String> attr, Point mark, boolean vertical) {
+            super();
+            this.mRect = rect;
+            this.mAttr = attr;
+            this.mMark = mark;
+            this.mVertical = vertical;
+        }
+
+        public DropZone(Rect rect, List<String> attr) {
+            this(rect, attr, null, false);
+        }
+
+        private Rect getRect() {
+            return mRect;
+        }
+
+        private List<String> getAttr() {
+            return mAttr;
+        }
+
+        private Point getMark() {
+            return mMark;
+        }
+
+        private boolean isVertical() {
+            return mVertical;
+        }
+
+        private boolean isBorderZone() {
+            return mMark != null;
+        }
+    }
+}