aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java878
1 files changed, 878 insertions, 0 deletions
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());
+ }
+ }
+}