diff options
author | Tor Norbye <tnorbye@google.com> | 2012-08-07 11:48:03 -0700 |
---|---|---|
committer | Tor Norbye <tnorbye@google.com> | 2012-08-08 15:18:48 -0700 |
commit | 0cb8e647ef345f5c2a6b7eb08b517421131bca4e (patch) | |
tree | 0952bed29c8c937cb6a2e0a6339efbe234e3e9a7 | |
parent | 880f28a8a6f1abde140e5a201e5d38f62a8db1ae (diff) | |
download | sdk-0cb8e647ef345f5c2a6b7eb08b517421131bca4e.tar.gz |
Improvements to relative layout move and delete operations
This changeset improves the way the RelativeLayout editing support in
the layout editor handles deletions and moves.
First, during a move, if the move is simply within the same layout,
then the layout constraints are left alone such that if you for
example have
A
v
B < C < D
and you move B up to be next to A, you end up with
A < B < C < D
(It will however remove cycles if the move would result in them.)
Second, it now handles deletion better where deleting a view will
cause all references to any deleted views to be replaced by transitive
constraints.
For example, if you hve
A < B < C < D
and you delete B and C, you end up with
A < D
Change-Id: Icb9d3552e60aee20192f7941fe52be71ba52557f
14 files changed, 883 insertions, 89 deletions
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 index 4f2590293..40063f2a2 100644 --- 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 @@ -403,8 +403,9 @@ public class GridLayoutRule extends BaseLayoutRule { } @Override - public void onRemovingChildren(@NonNull List<INode> deleted, @NonNull INode parent) { - super.onRemovingChildren(deleted, parent); + public void onRemovingChildren(@NonNull List<INode> deleted, @NonNull INode parent, + boolean moved) { + super.onRemovingChildren(deleted, parent, moved); // Attempt to clean up spacer objects for any newly-empty rows or columns // as the result of this deletion diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java index e23f9f455..850d0177b 100644 --- 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 @@ -17,7 +17,6 @@ package com.android.ide.common.layout; import static com.android.ide.common.layout.LayoutConstants.ATTR_GRAVITY; -import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ABOVE; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BOTTOM; @@ -36,8 +35,6 @@ import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_V import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF; -import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX; -import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE; import static com.android.utils.XmlUtils.ANDROID_URI; @@ -48,7 +45,6 @@ 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.INode.IAttribute; import com.android.ide.common.api.INodeHandler; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.InsertType; @@ -57,6 +53,7 @@ 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; @@ -66,10 +63,8 @@ import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -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 @@ -161,7 +156,7 @@ public class RelativeLayoutRule extends BaseLayoutRule { @Override public DropFeedback onDropMove(@NonNull INode targetNode, @NonNull IDragElement[] elements, @Nullable DropFeedback feedback, @NonNull Point p) { - if (elements == null || elements.length == 0) { + if (elements == null || elements.length == 0 || feedback == null) { return null; } @@ -184,6 +179,10 @@ public class RelativeLayoutRule extends BaseLayoutRule { @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, @@ -251,40 +250,14 @@ public class RelativeLayoutRule extends BaseLayoutRule { } @Override - public void onRemovingChildren(@NonNull List<INode> deleted, @NonNull INode parent) { - super.onRemovingChildren(deleted, parent); - - // Remove any attachments pointing to the deleted nodes. - - // Produce set of attribute values that we want to delete if - // present in a layout attribute - Set<String> removeValues = new HashSet<String>(deleted.size() * 2); - for (INode node : deleted) { - String id = node.getStringAttr(ANDROID_URI, ATTR_ID); - if (id != null) { - removeValues.add(id); - if (id.startsWith(NEW_ID_PREFIX)) { - removeValues.add(ID_PREFIX + stripIdPrefix(id)); - } else { - removeValues.add(NEW_ID_PREFIX + stripIdPrefix(id)); - } - } - } - - for (INode child : parent.getChildren()) { - if (deleted.contains(child)) { - continue; - } - for (IAttribute attribute : child.getLiveAttributes()) { - if (attribute.getName().startsWith(ATTR_LAYOUT_PREFIX) && - ANDROID_URI.equals(attribute.getUri())) { - String value = attribute.getValue(); - if (removeValues.contains(value)) { - // Unset this reference to a deleted widget. - child.setAttribute(ANDROID_URI, attribute.getName(), null); - } - } - } + 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(); } } @@ -303,6 +276,10 @@ public class RelativeLayoutRule extends BaseLayoutRule { 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); } @@ -310,6 +287,9 @@ public class RelativeLayoutRule extends BaseLayoutRule { @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() { 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 index 7c6fae840..bb320861c 100644 --- 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 @@ -40,6 +40,8 @@ import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_V import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.ide.common.api.SegmentType; import java.util.HashMap; @@ -138,7 +140,8 @@ enum ConstraintType { * @param attribute the name of the attribute to look up * @return the corresponding {@link ConstraintType} */ - public static ConstraintType fromAttribute(String attribute) { + @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); @@ -169,6 +172,7 @@ enum ConstraintType { * @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) { 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..30f12c06a --- /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.ide.common.layout.BaseViewRule.stripIdPrefix; +import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX; +import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX; +import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; +import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_CENTER_HORIZONTAL; +import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_CENTER_VERTICAL; +import static com.android.utils.XmlUtils.ANDROID_URI; + +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.android.ide.common.layout.LayoutConstants; +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 LayoutConstants#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_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/GuidelineHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelineHandler.java index e6b50ba0e..135cabe79 100644 --- 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 @@ -51,7 +51,6 @@ import static com.android.ide.common.layout.LayoutConstants.VALUE_N_DP; import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE; import static com.android.ide.common.layout.relative.ConstraintType.ALIGN_BASELINE; import static com.android.utils.XmlUtils.ANDROID_URI; - import static java.lang.Math.abs; import com.android.ide.common.api.DropFeedback; @@ -292,6 +291,11 @@ public class GuidelineHandler { 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; @@ -658,6 +662,7 @@ public class GuidelineHandler { } } + /** Breaks any cycles detected by the handler */ public void removeCycles() { if (mHorizontalCycle != null) { removeCycles(mHorizontalDeps); 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 index 7b237a2f4..cc1953ac2 100644 --- 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 @@ -25,7 +25,6 @@ import static com.android.ide.common.api.SegmentType.RIGHT; import static com.android.ide.common.api.SegmentType.TOP; import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; import static com.android.utils.XmlUtils.ANDROID_URI; - import static java.lang.Math.abs; import com.android.ide.common.api.DropFeedback; @@ -49,8 +48,15 @@ import java.util.List; * edges, and so on -- and picks the best among these. */ public class MoveHandler extends GuidelineHandler { - public int mDraggedBaseline; - + 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); @@ -159,6 +165,15 @@ public class MoveHandler extends GuidelineHandler { } } + /** + * 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; 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 index d1d8f37b6..0e4472420 100644 --- 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 @@ -25,7 +25,6 @@ import static com.android.ide.common.api.SegmentType.RIGHT; import static com.android.ide.common.api.SegmentType.TOP; import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; import static com.android.utils.XmlUtils.ANDROID_URI; - import static java.lang.Math.abs; import com.android.ide.common.api.DropFeedback; @@ -44,10 +43,18 @@ import java.util.Set; * edges in a RelativeLayout. */ public class ResizeHandler extends GuidelineHandler { - public final INode mResized; - public final SegmentType mHorizontalEdgeType; - public final SegmentType mVerticalEdgeType; - + 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) { @@ -58,7 +65,6 @@ public class ResizeHandler extends GuidelineHandler { assert horizontalEdgeType != CENTER_HORIZONTAL && verticalEdgeType != CENTER_HORIZONTAL; assert horizontalEdgeType != CENTER_VERTICAL && verticalEdgeType != CENTER_VERTICAL; - mResized = resized; mHorizontalEdgeType = horizontalEdgeType; mVerticalEdgeType = verticalEdgeType; @@ -162,6 +168,14 @@ public class ResizeHandler extends GuidelineHandler { 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; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java index 21e005f59..dbb414460 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java @@ -142,9 +142,7 @@ public class ClipboardSupport { */ public void cutSelectionToClipboard(List<SelectionItem> selection) { copySelectionToClipboard(selection); - deleteSelection( - mCanvas.getCutLabel(), - selection); + deleteSelection(mCanvas.getCutLabel(), selection); } /** @@ -170,9 +168,11 @@ public class ClipboardSupport { for (SelectionItem cs : selection) { CanvasViewInfo vi = cs.getViewInfo(); if (vi != null && vi.getParent() != null) { + CanvasViewInfo parent = vi.getParent(); + assert parent != null; if (title == null) { - title = vi.getParent().getName(); - } else if (!title.equals(vi.getParent().getName())) { + title = parent.getName(); + } else if (!title.equals(parent.getName())) { // More than one kind of parent selected. title = null; break; @@ -210,6 +210,9 @@ public class ClipboardSupport { new HashMap<NodeProxy, List<INode>>(); for (SelectionItem cs : selection) { NodeProxy node = cs.getNode(); + if (node == null) { + continue; + } INode parent = node.getParent(); if (parent != null) { List<INode> children = clusters.get(parent); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java index c2a535e2b..520396db8 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java @@ -404,6 +404,7 @@ public class MoveGesture extends DropGesture { if (event.detail == DND.DROP_MOVE) { GlobalCanvasDragInfo.getInstance().removeSource(); } + mTargetNode.applyPendingChanges(); } }); } finally { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java index a8438d8ea..7dc908b08 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java @@ -558,7 +558,8 @@ public class RulesEngine { IViewRule parentRule = loadRule(parentUiNode); if (parentRule != null) { try { - parentRule.onRemovingChildren(children, parentNode); + parentRule.onRemovingChildren(children, parentNode, + mInsertType == InsertType.MOVE_WITHIN); } catch (Exception e) { AdtPlugin.log(e, "%s.onDispose() failed: %s", parentRule.getClass().getSimpleName(), diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java index b9176f6c0..372e329c2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java @@ -36,8 +36,10 @@ import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; import com.google.common.base.Splitter; +import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; import java.io.IOException; import java.io.StringWriter; @@ -336,8 +338,24 @@ public class TestNode implements INode { } @Override + public @NonNull IAttribute[] getLiveAttributes() { + List<IAttribute> result = new ArrayList<IAttribute>(); + + NamedNodeMap attributes = mElement.getAttributes(); + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Attr attribute = (Attr) attributes.item(i); + result.add(new TestXmlAttribute(attribute)); + } + return result.toArray(new IAttribute[result.size()]); + } + + @Override public boolean setAttribute(String uri, String localName, String value) { - mElement.setAttributeNS(uri, localName, value); + if (value == null) { + mElement.removeAttributeNS(uri, localName); + } else { + mElement.setAttributeNS(uri, localName, value); + } return super.setAttribute(uri, localName, value); } @@ -395,6 +413,33 @@ public class TestNode implements INode { } } + public static class TestXmlAttribute implements IAttribute { + private Attr mAttribute; + + public TestXmlAttribute(Attr attribute) { + this.mAttribute = attribute; + } + + @Override + public String getUri() { + return mAttribute.getNamespaceURI(); + } + + @Override + public String getName() { + String name = mAttribute.getLocalName(); + if (name == null) { + name = mAttribute.getName(); + } + return name; + } + + @Override + public String getValue() { + return mAttribute.getValue(); + } + } + // Recursively initialize this node with the bounds specified in the given hierarchy // dump (from ViewHierarchy's DUMP_INFO flag public void assignBounds(String bounds) { diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/relative/DeletionHandlerTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/relative/DeletionHandlerTest.java new file mode 100644 index 000000000..f5ec1ba24 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/relative/DeletionHandlerTest.java @@ -0,0 +1,445 @@ +/* + * 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.ide.common.layout.LayoutConstants.ATTR_ID; +import static com.android.tools.lint.detector.api.LintConstants.ANDROID_URI; + +import com.android.ide.common.api.INode; +import com.android.ide.common.layout.BaseViewRule; +import com.android.ide.common.layout.TestNode; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import junit.framework.TestCase; + +@SuppressWarnings("javadoc") +public class DeletionHandlerTest extends TestCase { + public void testSimple() { + String xml = "" + + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + + "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " xmlns:tools=\"http://schemas.android.com/tools\"\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"match_parent\"\n" + + " tools:ignore=\"HardcodedText\" >\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button1\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignParentLeft=\"true\"\n" + + " android:text=\"A\" />\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button2\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBaseline=\"@+id/button1\"\n" + + " android:layout_alignBottom=\"@+id/button1\"\n" + + " android:layout_toRightOf=\"@+id/button1\"\n" + + " android:text=\"B\" />\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button3\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBottom=\"@+id/button2\"\n" + + " android:layout_toRightOf=\"@+id/button2\"\n" + + " android:text=\"C\" />\n" + + "\n" + + "</RelativeLayout>"; + TestNode targetNode = TestNode.createFromXml(xml); + assertNotNull(targetNode); + + TestNode button2 = TestNode.findById(targetNode, "@+id/button2"); + + INode layout = button2.getParent(); + List<INode> deletedNodes = Collections.<INode>singletonList(button2); + List<INode> movedNodes = Collections.<INode>emptyList(); + assertSame(layout, targetNode); + layout.removeChild(button2); + + DeletionHandler handler = new DeletionHandler(deletedNodes, movedNodes, layout); + handler.updateConstraints(); + + String updated = TestNode.toXml(targetNode); + assertEquals( + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + + "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " xmlns:tools=\"http://schemas.android.com/tools\"\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"match_parent\"\n" + + " tools:ignore=\"HardcodedText\">\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button1\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignParentLeft=\"true\"\n" + + " android:text=\"A\">\n" + + " </Button>\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button3\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBaseline=\"@+id/button1\"\n" + + " android:layout_alignBottom=\"@+id/button1\"\n" + + " android:layout_toRightOf=\"@+id/button1\"\n" + + " android:text=\"C\">\n" + + " </Button>\n" + + "\n" + + "</RelativeLayout>", + updated); + assertFalse(updated.contains(BaseViewRule.stripIdPrefix(button2.getStringAttr(ANDROID_URI, + ATTR_ID)))); + } + + public void testTransitive() { + String xml = "" + + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + + "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " xmlns:tools=\"http://schemas.android.com/tools\"\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"match_parent\"\n" + + " tools:ignore=\"HardcodedText\" >\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button1\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignParentLeft=\"true\"\n" + + " android:layout_alignParentTop=\"true\"\n" + + " android:text=\"Above\" />\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button2\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignParentLeft=\"true\"\n" + + " android:layout_below=\"@+id/button1\"\n" + + " android:text=\"A\" />\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button3\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBaseline=\"@+id/button2\"\n" + + " android:layout_alignBottom=\"@+id/button2\"\n" + + " android:layout_toRightOf=\"@+id/button2\"\n" + + " android:text=\"B\" />\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button4\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBottom=\"@+id/button3\"\n" + + " android:layout_toRightOf=\"@+id/button3\"\n" + + " android:text=\"C\" />\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button5\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBaseline=\"@+id/button4\"\n" + + " android:layout_alignBottom=\"@+id/button4\"\n" + + " android:layout_toRightOf=\"@+id/button4\"\n" + + " android:text=\"D\" />\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button6\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBottom=\"@+id/button5\"\n" + + " android:layout_toRightOf=\"@+id/button5\"\n" + + " android:text=\"E\" />\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button7\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignLeft=\"@+id/button3\"\n" + + " android:layout_below=\"@+id/button3\"\n" + + " android:text=\"Button\" />\n" + + "\n" + + " <CheckBox\n" + + " android:id=\"@+id/checkBox1\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBaseline=\"@+id/button7\"\n" + + " android:layout_alignBottom=\"@+id/button7\"\n" + + " android:layout_toRightOf=\"@+id/button7\"\n" + + " android:text=\"CheckBox\" />\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button8\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_below=\"@+id/checkBox1\"\n" + + " android:layout_toRightOf=\"@+id/checkBox1\"\n" + + " android:text=\"Button\" />\n" + + "\n" + + "</RelativeLayout>"; + TestNode targetNode = TestNode.createFromXml(xml); + assertNotNull(targetNode); + TestNode button7 = TestNode.findById(targetNode, "@+id/button7"); + TestNode checkBox = TestNode.findById(targetNode, "@+id/checkBox1"); + + INode layout = button7.getParent(); + List<INode> deletedNodes = Arrays.<INode>asList(button7, checkBox); + List<INode> movedNodes = Collections.<INode>emptyList(); + assertSame(layout, targetNode); + layout.removeChild(button7); + layout.removeChild(checkBox); + + DeletionHandler handler = new DeletionHandler(deletedNodes, movedNodes, layout); + handler.updateConstraints(); + + String updated = TestNode.toXml(targetNode); + assertEquals( + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + + "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " xmlns:tools=\"http://schemas.android.com/tools\"\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"match_parent\"\n" + + " tools:ignore=\"HardcodedText\">\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button1\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignParentLeft=\"true\"\n" + + " android:layout_alignParentTop=\"true\"\n" + + " android:text=\"Above\">\n" + + " </Button>\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button2\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignParentLeft=\"true\"\n" + + " android:layout_below=\"@+id/button1\"\n" + + " android:text=\"A\">\n" + + " </Button>\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button3\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBaseline=\"@+id/button2\"\n" + + " android:layout_alignBottom=\"@+id/button2\"\n" + + " android:layout_toRightOf=\"@+id/button2\"\n" + + " android:text=\"B\">\n" + + " </Button>\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button4\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBottom=\"@+id/button3\"\n" + + " android:layout_toRightOf=\"@+id/button3\"\n" + + " android:text=\"C\">\n" + + " </Button>\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button5\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBaseline=\"@+id/button4\"\n" + + " android:layout_alignBottom=\"@+id/button4\"\n" + + " android:layout_toRightOf=\"@+id/button4\"\n" + + " android:text=\"D\">\n" + + " </Button>\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button6\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBottom=\"@+id/button5\"\n" + + " android:layout_toRightOf=\"@+id/button5\"\n" + + " android:text=\"E\">\n" + + " </Button>\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button8\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignLeft=\"@+id/button3\"\n" + + " android:layout_below=\"@+id/button3\"\n" + + " android:text=\"Button\">\n" + + " </Button>\n" + + "\n" + + "</RelativeLayout>", + updated); + assertFalse(updated.contains(BaseViewRule.stripIdPrefix(button7.getStringAttr(ANDROID_URI, + ATTR_ID)))); + } + + public void testCenter() { + String xml = + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + + "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " xmlns:tools=\"http://schemas.android.com/tools\"\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"match_parent\"\n" + + " tools:ignore=\"HardcodedText\" >\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button1\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_centerInParent=\"true\"\n" + + " android:text=\"Button\" />\n" + + "\n" + + " <CheckBox\n" + + " android:id=\"@+id/checkBox1\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_below=\"@+id/button1\"\n" + + " android:layout_toRightOf=\"@+id/button1\"\n" + + " android:text=\"CheckBox\" />\n" + + "\n" + + "</RelativeLayout>"; + + TestNode targetNode = TestNode.createFromXml(xml); + assertNotNull(targetNode); + TestNode button1 = TestNode.findById(targetNode, "@+id/button1"); + + INode layout = button1.getParent(); + List<INode> deletedNodes = Collections.<INode>singletonList(button1); + List<INode> movedNodes = Collections.<INode>emptyList(); + assertSame(layout, targetNode); + layout.removeChild(button1); + + DeletionHandler handler = new DeletionHandler(deletedNodes, movedNodes, layout); + handler.updateConstraints(); + + String updated = TestNode.toXml(targetNode); + assertEquals( + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + + "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " xmlns:tools=\"http://schemas.android.com/tools\"\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"match_parent\"\n" + + " tools:ignore=\"HardcodedText\">\n" + + "\n" + + " <CheckBox\n" + + " android:id=\"@+id/checkBox1\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_centerInParent=\"true\"\n" + + " android:text=\"CheckBox\">\n" + + " </CheckBox>\n" + + "\n" + + "</RelativeLayout>", + updated); + assertFalse(updated.contains(BaseViewRule.stripIdPrefix(button1.getStringAttr(ANDROID_URI, + ATTR_ID)))); + + } + + public void testMove() { + String xml = "" + + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + + "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " xmlns:tools=\"http://schemas.android.com/tools\"\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"match_parent\"\n" + + " tools:ignore=\"HardcodedText\" >\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button1\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignParentLeft=\"true\"\n" + + " android:text=\"A\" />\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button2\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBaseline=\"@+id/button1\"\n" + + " android:layout_alignBottom=\"@+id/button1\"\n" + + " android:layout_toRightOf=\"@+id/button1\"\n" + + " android:text=\"B\" />\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button3\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBottom=\"@+id/button2\"\n" + + " android:layout_toRightOf=\"@+id/button2\"\n" + + " android:text=\"C\" />\n" + + "\n" + + "</RelativeLayout>"; + TestNode targetNode = TestNode.createFromXml(xml); + assertNotNull(targetNode); + + TestNode button2 = TestNode.findById(targetNode, "@+id/button2"); + + INode layout = button2.getParent(); + List<INode> deletedNodes = Collections.<INode>singletonList(button2); + List<INode> movedNodes = Collections.<INode>singletonList(button2); + assertSame(layout, targetNode); + + DeletionHandler handler = new DeletionHandler(deletedNodes, movedNodes, layout); + handler.updateConstraints(); + + String updated = TestNode.toXml(targetNode); + assertEquals( + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + + "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " xmlns:tools=\"http://schemas.android.com/tools\"\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"match_parent\"\n" + + " tools:ignore=\"HardcodedText\">\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button1\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignParentLeft=\"true\"\n" + + " android:text=\"A\">\n" + + " </Button>\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button2\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBaseline=\"@+id/button1\"\n" + + " android:layout_alignBottom=\"@+id/button1\"\n" + + " android:layout_toRightOf=\"@+id/button1\"\n" + + " android:text=\"B\">\n" + + " </Button>\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button3\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignBottom=\"@+id/button2\"\n" + + " android:layout_toRightOf=\"@+id/button2\"\n" + + " android:text=\"C\">\n" + + " </Button>\n" + + "\n" + + "</RelativeLayout>", + updated); + assertTrue(updated.contains(BaseViewRule.stripIdPrefix(button2.getStringAttr(ANDROID_URI, + ATTR_ID)))); + } +} diff --git a/rule_api/src/com/android/ide/common/api/AbstractViewRule.java b/rule_api/src/com/android/ide/common/api/AbstractViewRule.java index 7f058095b..068580fc8 100644 --- a/rule_api/src/com/android/ide/common/api/AbstractViewRule.java +++ b/rule_api/src/com/android/ide/common/api/AbstractViewRule.java @@ -47,8 +47,6 @@ public class AbstractViewRule implements IViewRule { return null; } - // ==== Selection ==== - @Override @Nullable public List<String> getSelectionHint(@NonNull INode parentNode, @NonNull INode childNode) { @@ -69,12 +67,10 @@ public class AbstractViewRule implements IViewRule { @NonNull List<? extends INode> childNodes, @Nullable Object view) { } - // ==== Drag & drop support ==== - - // By default Views do not accept drag'n'drop. @Override @Nullable - public DropFeedback onDropEnter(@NonNull INode targetNode, @Nullable Object targetView, @Nullable IDragElement[] elements) { + public DropFeedback onDropEnter(@NonNull INode targetNode, @Nullable Object targetView, + @Nullable IDragElement[] elements) { return null; } @@ -86,7 +82,8 @@ public class AbstractViewRule implements IViewRule { } @Override - public void onDropLeave(@NonNull INode targetNode, @NonNull IDragElement[] elements, @Nullable DropFeedback feedback) { + public void onDropLeave(@NonNull INode targetNode, @NonNull IDragElement[] elements, + @Nullable DropFeedback feedback) { // ignore } @@ -101,39 +98,42 @@ public class AbstractViewRule implements IViewRule { @Override - public void onPaste(@NonNull INode targetNode, @Nullable Object targetView, @NonNull IDragElement[] pastedElements) { + public void onPaste(@NonNull INode targetNode, @Nullable Object targetView, + @NonNull IDragElement[] pastedElements) { } - // ==== Create/Remove hooks ==== - @Override - public void onCreate(@NonNull INode node, @NonNull INode parent, @NonNull InsertType insertType) { + public void onCreate(@NonNull INode node, @NonNull INode parent, + @NonNull InsertType insertType) { } @Override - public void onChildInserted(@NonNull INode child, @NonNull INode parent, @NonNull InsertType insertType) { + public void onChildInserted(@NonNull INode child, @NonNull INode parent, + @NonNull InsertType insertType) { } @Override - public void onRemovingChildren(@NonNull List<INode> deleted, @NonNull INode parent) { + public void onRemovingChildren(@NonNull List<INode> deleted, @NonNull INode parent, + boolean moved) { } - // ==== Resizing ==== - @Override @Nullable - public DropFeedback onResizeBegin(@NonNull INode child, @NonNull INode parent, @Nullable SegmentType horizontalEdge, - @Nullable SegmentType verticalEdge, @Nullable Object childView, @Nullable Object parentView) { + public DropFeedback onResizeBegin(@NonNull INode child, @NonNull INode parent, + @Nullable SegmentType horizontalEdge, + @Nullable SegmentType verticalEdge, @Nullable Object childView, + @Nullable Object parentView) { return null; } @Override - public void onResizeUpdate(@Nullable DropFeedback feedback, @NonNull INode child, @NonNull INode parent, @NonNull Rect newBounds, + public void onResizeUpdate(@Nullable DropFeedback feedback, @NonNull INode child, + @NonNull INode parent, @NonNull Rect newBounds, int modifierMask) { } @Override - public void onResizeEnd(@Nullable DropFeedback feedback, @NonNull INode child, final @NonNull INode parent, - final @NonNull Rect newBounds) { + public void onResizeEnd(@Nullable DropFeedback feedback, @NonNull INode child, + @NonNull INode parent, @NonNull Rect newBounds) { } } diff --git a/rule_api/src/com/android/ide/common/api/IViewRule.java b/rule_api/src/com/android/ide/common/api/IViewRule.java index c11579507..b16df266c 100644 --- a/rule_api/src/com/android/ide/common/api/IViewRule.java +++ b/rule_api/src/com/android/ide/common/api/IViewRule.java @@ -298,19 +298,32 @@ public interface IViewRule { @NonNull InsertType insertType); /** - * Called when one or more children are about to be deleted by the user. Note that - * children deleted programmatically from view rules (via + * Called when one or more children are about to be deleted by the user. + * Note that children deleted programmatically from view rules (via * {@link INode#removeChild(INode)}) will not notify about deletion. * <p> - * Note that this method will be called under an edit lock, so rules can directly - * add/remove nodes and attributes as part of the deletion handling (and their - * actions will be part of the same undo-unit.) + * Note that this method will be called under an edit lock, so rules can + * directly add/remove nodes and attributes as part of the deletion handling + * (and their actions will be part of the same undo-unit.) + * <p> + * Note that when children are moved (such as when you drag a child within a + * LinearLayout to move it from one position among the children to another), + * that will also result in a + * {@link #onChildInserted(INode, INode, InsertType)} (with the + * {@code InsertType} set to {@link InsertType#MOVE_WITHIN}) and a remove + * via this {@link #onRemovingChildren(List, INode, boolean)} method. When + * the deletion is occurring as part of a local move (insert + delete), the + * {@code moved} parameter to this method is set to true. * * @param deleted a nonempty list of children about to be deleted - * @param parent the parent of the deleted children (which still contains the children - * since this method is called before the deletion is performed) + * @param parent the parent of the deleted children (which still contains + * the children since this method is called before the deletion + * is performed) + * @param moved when true, the nodes are being deleted as part of a local + * move (where copies are inserted elsewhere) */ - void onRemovingChildren(@NonNull List<INode> deleted, @NonNull INode parent); + void onRemovingChildren(@NonNull List<INode> deleted, @NonNull INode parent, + boolean moved); /** * Called by the IDE on the parent layout when a child widget is being resized. This |