1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.eclipse.org/org/documents/epl-v10.php
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.ide.common.layout;
18
19import static com.android.SdkConstants.ANDROID_URI;
20import static com.android.SdkConstants.ATTR_ID;
21import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE;
22import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
23import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
24import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
25import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
26import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
27import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
28import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
29import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
30import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
31import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING;
32import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
33import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
34import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_IN_PARENT;
35import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL;
36import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
37import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
38import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
39import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
40import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
41import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM;
42import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
43import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT;
44import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
45import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
46import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
47import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
48import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
49import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
50import static com.android.SdkConstants.ATTR_LAYOUT_X;
51import static com.android.SdkConstants.ATTR_LAYOUT_Y;
52import static com.android.SdkConstants.VALUE_FILL_PARENT;
53import static com.android.SdkConstants.VALUE_MATCH_PARENT;
54import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
55
56import com.android.SdkConstants;
57import com.android.annotations.NonNull;
58import com.android.annotations.Nullable;
59import com.android.ide.common.api.DrawingStyle;
60import com.android.ide.common.api.DropFeedback;
61import com.android.ide.common.api.IAttributeInfo;
62import com.android.ide.common.api.IClientRulesEngine;
63import com.android.ide.common.api.IDragElement;
64import com.android.ide.common.api.IDragElement.IDragAttribute;
65import com.android.ide.common.api.IFeedbackPainter;
66import com.android.ide.common.api.IGraphics;
67import com.android.ide.common.api.IMenuCallback;
68import com.android.ide.common.api.INode;
69import com.android.ide.common.api.INodeHandler;
70import com.android.ide.common.api.IViewRule;
71import com.android.ide.common.api.MarginType;
72import com.android.ide.common.api.Point;
73import com.android.ide.common.api.Rect;
74import com.android.ide.common.api.RuleAction;
75import com.android.ide.common.api.RuleAction.ChoiceProvider;
76import com.android.ide.common.api.Segment;
77import com.android.ide.common.api.SegmentType;
78import com.android.utils.Pair;
79
80import java.net.URL;
81import java.util.Arrays;
82import java.util.Collections;
83import java.util.HashMap;
84import java.util.HashSet;
85import java.util.List;
86import java.util.Map;
87import java.util.Set;
88
89/**
90 * A {@link IViewRule} for all layouts.
91 */
92public class BaseLayoutRule extends BaseViewRule {
93    private static final String ACTION_FILL_WIDTH = "_fillW";  //$NON-NLS-1$
94    private static final String ACTION_FILL_HEIGHT = "_fillH"; //$NON-NLS-1$
95    private static final String ACTION_MARGIN = "_margin";     //$NON-NLS-1$
96    private static final URL ICON_MARGINS =
97        BaseLayoutRule.class.getResource("margins.png"); //$NON-NLS-1$
98    private static final URL ICON_GRAVITY =
99        BaseLayoutRule.class.getResource("gravity.png"); //$NON-NLS-1$
100    private static final URL ICON_FILL_WIDTH =
101        BaseLayoutRule.class.getResource("fillwidth.png"); //$NON-NLS-1$
102    private static final URL ICON_FILL_HEIGHT =
103        BaseLayoutRule.class.getResource("fillheight.png"); //$NON-NLS-1$
104
105    // ==== Layout Actions support ====
106
107    // The Margin layout parameters are available for LinearLayout, FrameLayout, RelativeLayout,
108    // and their subclasses.
109    protected final RuleAction createMarginAction(final INode parentNode,
110            final List<? extends INode> children) {
111
112        final List<? extends INode> targets = children == null || children.size() == 0 ?
113                Collections.singletonList(parentNode)
114                : children;
115        final INode first = targets.get(0);
116
117        IMenuCallback actionCallback = new IMenuCallback() {
118            @Override
119            public void action(@NonNull RuleAction action,
120                    @NonNull List<? extends INode> selectedNodes,
121                    final @Nullable String valueId,
122                    final @Nullable Boolean newValue) {
123                parentNode.editXml("Change Margins", new INodeHandler() {
124                    @Override
125                    public void handle(@NonNull INode n) {
126                        String uri = ANDROID_URI;
127                        String all = first.getStringAttr(uri, ATTR_LAYOUT_MARGIN);
128                        String left = first.getStringAttr(uri, ATTR_LAYOUT_MARGIN_LEFT);
129                        String right = first.getStringAttr(uri, ATTR_LAYOUT_MARGIN_RIGHT);
130                        String top = first.getStringAttr(uri, ATTR_LAYOUT_MARGIN_TOP);
131                        String bottom = first.getStringAttr(uri, ATTR_LAYOUT_MARGIN_BOTTOM);
132                        String[] margins = mRulesEngine.displayMarginInput(all, left,
133                                right, top, bottom);
134                        if (margins != null) {
135                            assert margins.length == 5;
136                            for (INode child : targets) {
137                                child.setAttribute(uri, ATTR_LAYOUT_MARGIN, margins[0]);
138                                child.setAttribute(uri, ATTR_LAYOUT_MARGIN_LEFT, margins[1]);
139                                child.setAttribute(uri, ATTR_LAYOUT_MARGIN_RIGHT, margins[2]);
140                                child.setAttribute(uri, ATTR_LAYOUT_MARGIN_TOP, margins[3]);
141                                child.setAttribute(uri, ATTR_LAYOUT_MARGIN_BOTTOM, margins[4]);
142                            }
143                        }
144                    }
145                });
146            }
147        };
148
149        return RuleAction.createAction(ACTION_MARGIN, "Change Margins...", actionCallback,
150                ICON_MARGINS, 40, false);
151    }
152
153    // Both LinearLayout and RelativeLayout have a gravity (but RelativeLayout applies it
154    // to the parent whereas for LinearLayout it's on the children)
155    protected final RuleAction createGravityAction(final List<? extends INode> targets, final
156            String attributeName) {
157        if (targets != null && targets.size() > 0) {
158            final INode first = targets.get(0);
159            ChoiceProvider provider = new ChoiceProvider() {
160                @Override
161                public void addChoices(@NonNull List<String> titles, @NonNull List<URL> iconUrls,
162                        @NonNull List<String> ids) {
163                    IAttributeInfo info = first.getAttributeInfo(ANDROID_URI, attributeName);
164                    if (info != null) {
165                        // Generate list of possible gravity value constants
166                        assert info.getFormats().contains(IAttributeInfo.Format.FLAG);
167                        for (String name : info.getFlagValues()) {
168                            titles.add(getAttributeDisplayName(name));
169                            ids.add(name);
170                        }
171                    }
172                }
173            };
174
175            return RuleAction.createChoices("_gravity", "Change Gravity", //$NON-NLS-1$
176                    new PropertyCallback(targets, "Change Gravity", ANDROID_URI,
177                            attributeName),
178                    provider,
179                    first.getStringAttr(ANDROID_URI, attributeName), ICON_GRAVITY,
180                    43, false);
181        }
182
183        return null;
184    }
185
186    @Override
187    public void addLayoutActions(
188            @NonNull List<RuleAction> actions,
189            final @NonNull INode parentNode,
190            final @NonNull List<? extends INode> children) {
191        super.addLayoutActions(actions, parentNode, children);
192
193        final List<? extends INode> targets = children == null || children.size() == 0 ?
194                Collections.singletonList(parentNode)
195                : children;
196        final INode first = targets.get(0);
197
198        // Shared action callback
199        IMenuCallback actionCallback = new IMenuCallback() {
200            @Override
201            public void action(
202                    @NonNull RuleAction action,
203                    @NonNull List<? extends INode> selectedNodes,
204                    final @Nullable String valueId,
205                    final @Nullable Boolean newValue) {
206                final String actionId = action.getId();
207                final String undoLabel;
208                if (actionId.equals(ACTION_FILL_WIDTH)) {
209                    undoLabel = "Change Width Fill";
210                } else if (actionId.equals(ACTION_FILL_HEIGHT)) {
211                    undoLabel = "Change Height Fill";
212                } else {
213                    return;
214                }
215                parentNode.editXml(undoLabel, new INodeHandler() {
216                    @Override
217                    public void handle(@NonNull INode n) {
218                        String attribute = actionId.equals(ACTION_FILL_WIDTH)
219                                ? ATTR_LAYOUT_WIDTH : ATTR_LAYOUT_HEIGHT;
220                        String value;
221                        if (newValue) {
222                            if (supportsMatchParent()) {
223                                value = VALUE_MATCH_PARENT;
224                            } else {
225                                value = VALUE_FILL_PARENT;
226                            }
227                        } else {
228                            value = VALUE_WRAP_CONTENT;
229                        }
230                        for (INode child : targets) {
231                            child.setAttribute(ANDROID_URI, attribute, value);
232                        }
233                    }
234                });
235            }
236        };
237
238        actions.add(RuleAction.createToggle(ACTION_FILL_WIDTH, "Toggle Fill Width",
239                isFilled(first, ATTR_LAYOUT_WIDTH), actionCallback, ICON_FILL_WIDTH, 10, false));
240        actions.add(RuleAction.createToggle(ACTION_FILL_HEIGHT, "Toggle Fill Height",
241                isFilled(first, ATTR_LAYOUT_HEIGHT), actionCallback, ICON_FILL_HEIGHT, 20, false));
242    }
243
244    // ==== Paste support ====
245
246    /**
247     * The default behavior for pasting in a layout is to simulate a drop in the
248     * top-left corner of the view.
249     * <p/>
250     * Note that we explicitly do not call super() here -- the BaseViewRule.onPaste handler
251     * will call onPasteBeforeChild() instead.
252     * <p/>
253     * Derived layouts should override this behavior if not appropriate.
254     */
255    @Override
256    public void onPaste(@NonNull INode targetNode, @Nullable Object targetView,
257            @NonNull IDragElement[] elements) {
258        DropFeedback feedback = onDropEnter(targetNode, targetView, elements);
259        if (feedback != null) {
260            Point p = targetNode.getBounds().getTopLeft();
261            feedback = onDropMove(targetNode, elements, feedback, p);
262            if (feedback != null) {
263                onDropLeave(targetNode, elements, feedback);
264                onDropped(targetNode, elements, feedback, p);
265            }
266        }
267    }
268
269    /**
270     * The default behavior for pasting in a layout with a specific child target
271     * is to simulate a drop right above the top left of the given child target.
272     * <p/>
273     * This method is invoked by BaseView when onPaste() is called --
274     * views don't generally accept children and instead use the target node as
275     * a hint to paste "before" it.
276     *
277     * @param parentNode the parent node we're pasting into
278     * @param parentView the view object for the parent layout, or null
279     * @param targetNode the first selected node
280     * @param elements the elements being pasted
281     */
282    public void onPasteBeforeChild(INode parentNode, Object parentView, INode targetNode,
283            IDragElement[] elements) {
284        DropFeedback feedback = onDropEnter(parentNode, parentView, elements);
285        if (feedback != null) {
286            Point parentP = parentNode.getBounds().getTopLeft();
287            Point targetP = targetNode.getBounds().getTopLeft();
288            if (parentP.y < targetP.y) {
289                targetP.y -= 1;
290            }
291
292            feedback = onDropMove(parentNode, elements, feedback, targetP);
293            if (feedback != null) {
294                onDropLeave(parentNode, elements, feedback);
295                onDropped(parentNode, elements, feedback, targetP);
296            }
297        }
298    }
299
300    // ==== Utility methods used by derived layouts ====
301
302    /**
303     * Draws the bounds of the given elements and all its children elements in the canvas
304     * with the specified offset.
305     *
306     * @param gc the graphics context
307     * @param element the element to be drawn
308     * @param offsetX a horizontal delta to add to the current bounds of the element when
309     *            drawing it
310     * @param offsetY a vertical delta to add to the current bounds of the element when
311     *            drawing it
312     */
313    public void drawElement(IGraphics gc, IDragElement element, int offsetX, int offsetY) {
314        Rect b = element.getBounds();
315        if (b.isValid()) {
316            gc.drawRect(b.x + offsetX, b.y + offsetY, b.x + offsetX + b.w, b.y + offsetY + b.h);
317        }
318
319        for (IDragElement inner : element.getInnerElements()) {
320            drawElement(gc, inner, offsetX, offsetY);
321        }
322    }
323
324    /**
325     * Collect all the "android:id" IDs from the dropped elements. When moving
326     * objects within the same canvas, that's all there is to do. However if the
327     * objects are moved to a different canvas or are copied then set
328     * createNewIds to true to find the existing IDs under targetNode and create
329     * a map with new non-conflicting unique IDs as needed. Returns a map String
330     * old-id => tuple (String new-id, String fqcn) where fqcn is the FQCN of
331     * the element.
332     */
333    protected static Map<String, Pair<String, String>> getDropIdMap(INode targetNode,
334            IDragElement[] elements, boolean createNewIds) {
335        Map<String, Pair<String, String>> idMap = new HashMap<String, Pair<String, String>>();
336
337        if (createNewIds) {
338            collectIds(idMap, elements);
339            // Need to remap ids if necessary
340            idMap = remapIds(targetNode, idMap);
341        }
342
343        return idMap;
344    }
345
346    /**
347     * Fills idMap with a map String id => tuple (String id, String fqcn) where
348     * fqcn is the FQCN of the element (in case we want to generate new IDs
349     * based on the element type.)
350     *
351     * @see #getDropIdMap
352     */
353    protected static Map<String, Pair<String, String>> collectIds(
354            Map<String, Pair<String, String>> idMap,
355            IDragElement[] elements) {
356        for (IDragElement element : elements) {
357            IDragAttribute attr = element.getAttribute(ANDROID_URI, ATTR_ID);
358            if (attr != null) {
359                String id = attr.getValue();
360                if (id != null && id.length() > 0) {
361                    idMap.put(id, Pair.of(id, element.getFqcn()));
362                }
363            }
364
365            collectIds(idMap, element.getInnerElements());
366        }
367
368        return idMap;
369    }
370
371    /**
372     * Used by #getDropIdMap to find new IDs in case of conflict.
373     */
374    protected static Map<String, Pair<String, String>> remapIds(INode node,
375            Map<String, Pair<String, String>> idMap) {
376        // Visit the document to get a list of existing ids
377        Set<String> existingIdSet = new HashSet<String>();
378        collectExistingIds(node.getRoot(), existingIdSet);
379
380        Map<String, Pair<String, String>> new_map = new HashMap<String, Pair<String, String>>();
381        for (Map.Entry<String, Pair<String, String>> entry : idMap.entrySet()) {
382            String key = entry.getKey();
383            Pair<String, String> value = entry.getValue();
384
385            String id = normalizeId(key);
386
387            if (!existingIdSet.contains(id)) {
388                // Not a conflict. Use as-is.
389                new_map.put(key, value);
390                if (!key.equals(id)) {
391                    new_map.put(id, value);
392                }
393            } else {
394                // There is a conflict. Get a new id.
395                String new_id = findNewId(value.getSecond(), existingIdSet);
396                value = Pair.of(new_id, value.getSecond());
397                new_map.put(id, value);
398                new_map.put(id.replaceFirst("@\\+", "@"), value); //$NON-NLS-1$ //$NON-NLS-2$
399            }
400        }
401
402        return new_map;
403    }
404
405    /**
406     * Used by #remapIds to find a new ID for a conflicting element.
407     */
408    protected static String findNewId(String fqcn, Set<String> existingIdSet) {
409        // Get the last component of the FQCN (e.g. "android.view.Button" =>
410        // "Button")
411        String name = fqcn.substring(fqcn.lastIndexOf('.') + 1);
412
413        for (int i = 1; i < 1000000; i++) {
414            String id = String.format("@+id/%s%02d", name, i); //$NON-NLS-1$
415            if (!existingIdSet.contains(id)) {
416                existingIdSet.add(id);
417                return id;
418            }
419        }
420
421        // We'll never reach here.
422        return null;
423    }
424
425    /**
426     * Used by #getDropIdMap to find existing IDs recursively.
427     */
428    protected static void collectExistingIds(INode root, Set<String> existingIdSet) {
429        if (root == null) {
430            return;
431        }
432
433        String id = root.getStringAttr(ANDROID_URI, ATTR_ID);
434        if (id != null) {
435            id = normalizeId(id);
436
437            if (!existingIdSet.contains(id)) {
438                existingIdSet.add(id);
439            }
440        }
441
442        for (INode child : root.getChildren()) {
443            collectExistingIds(child, existingIdSet);
444        }
445    }
446
447    /**
448     * Transforms @id/name into @+id/name to treat both forms the same way.
449     */
450    protected static String normalizeId(String id) {
451        if (id.indexOf("@+") == -1) { //$NON-NLS-1$
452            id = id.replaceFirst("@", "@+"); //$NON-NLS-1$ //$NON-NLS-2$
453        }
454        return id;
455    }
456
457    /**
458     * For use by {@link BaseLayoutRule#addAttributes} A filter should return a
459     * valid replacement string.
460     */
461    protected static interface AttributeFilter {
462        String replace(String attributeUri, String attributeName, String attributeValue);
463    }
464
465    private static final String[] EXCLUDED_ATTRIBUTES = new String[] {
466        // Common
467        ATTR_LAYOUT_GRAVITY,
468
469        // from AbsoluteLayout
470        ATTR_LAYOUT_X,
471        ATTR_LAYOUT_Y,
472
473        // from RelativeLayout
474        ATTR_LAYOUT_ABOVE,
475        ATTR_LAYOUT_BELOW,
476        ATTR_LAYOUT_TO_LEFT_OF,
477        ATTR_LAYOUT_TO_RIGHT_OF,
478        ATTR_LAYOUT_ALIGN_BASELINE,
479        ATTR_LAYOUT_ALIGN_TOP,
480        ATTR_LAYOUT_ALIGN_BOTTOM,
481        ATTR_LAYOUT_ALIGN_LEFT,
482        ATTR_LAYOUT_ALIGN_RIGHT,
483        ATTR_LAYOUT_ALIGN_PARENT_TOP,
484        ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
485        ATTR_LAYOUT_ALIGN_PARENT_LEFT,
486        ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
487        ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING,
488        ATTR_LAYOUT_CENTER_HORIZONTAL,
489        ATTR_LAYOUT_CENTER_IN_PARENT,
490        ATTR_LAYOUT_CENTER_VERTICAL,
491
492        // From GridLayout
493        ATTR_LAYOUT_ROW,
494        ATTR_LAYOUT_ROW_SPAN,
495        ATTR_LAYOUT_COLUMN,
496        ATTR_LAYOUT_COLUMN_SPAN
497    };
498
499    /**
500     * Default attribute filter used by the various layouts to filter out some properties
501     * we don't want to offer.
502     */
503    public static final AttributeFilter DEFAULT_ATTR_FILTER = new AttributeFilter() {
504        Set<String> mExcludes;
505
506        @Override
507        public String replace(String uri, String name, String value) {
508            if (!ANDROID_URI.equals(uri)) {
509                return value;
510            }
511
512            if (mExcludes == null) {
513                mExcludes = new HashSet<String>(EXCLUDED_ATTRIBUTES.length);
514                mExcludes.addAll(Arrays.asList(EXCLUDED_ATTRIBUTES));
515            }
516
517            return mExcludes.contains(name) ? null : value;
518        }
519    };
520
521    /**
522     * Copies all the attributes from oldElement to newNode. Uses the idMap to
523     * transform the value of all attributes of Format.REFERENCE. If filter is
524     * non-null, it's a filter that can rewrite the attribute string.
525     */
526    protected static void addAttributes(INode newNode, IDragElement oldElement,
527            Map<String, Pair<String, String>> idMap, AttributeFilter filter) {
528
529        for (IDragAttribute attr : oldElement.getAttributes()) {
530            String uri = attr.getUri();
531            String name = attr.getName();
532            String value = attr.getValue();
533
534            IAttributeInfo attrInfo = newNode.getAttributeInfo(uri, name);
535            if (attrInfo != null) {
536                if (attrInfo.getFormats().contains(IAttributeInfo.Format.REFERENCE)) {
537                    if (idMap.containsKey(value)) {
538                        value = idMap.get(value).getFirst();
539                    }
540                }
541            }
542
543            if (filter != null) {
544                value = filter.replace(uri, name, value);
545            }
546            if (value != null && value.length() > 0) {
547                newNode.setAttribute(uri, name, value);
548            }
549        }
550    }
551
552    /**
553     * Adds all the children elements of oldElement to newNode, recursively.
554     * Attributes are adjusted by calling addAttributes with idMap as necessary,
555     * with no closure filter.
556     */
557    protected static void addInnerElements(INode newNode, IDragElement oldElement,
558            Map<String, Pair<String, String>> idMap) {
559
560        for (IDragElement element : oldElement.getInnerElements()) {
561            String fqcn = element.getFqcn();
562            INode childNode = newNode.appendChild(fqcn);
563
564            addAttributes(childNode, element, idMap, null /* filter */);
565            addInnerElements(childNode, element, idMap);
566        }
567    }
568
569    /**
570     * Insert the given elements into the given node at the given position
571     *
572     * @param targetNode the node to insert into
573     * @param elements the elements to insert
574     * @param createNewIds if true, generate new ids when there is a conflict
575     * @param initialInsertPos index among targetnode's children which to insert the
576     *            children
577     */
578    public static void insertAt(final INode targetNode, final IDragElement[] elements,
579            final boolean createNewIds, final int initialInsertPos) {
580
581        // Collect IDs from dropped elements and remap them to new IDs
582        // if this is a copy or from a different canvas.
583        final Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements,
584                createNewIds);
585
586        targetNode.editXml("Insert Elements", new INodeHandler() {
587
588            @Override
589            public void handle(@NonNull INode node) {
590                // Now write the new elements.
591                int insertPos = initialInsertPos;
592                for (IDragElement element : elements) {
593                    String fqcn = element.getFqcn();
594
595                    INode newChild = targetNode.insertChildAt(fqcn, insertPos);
596
597                    // insertPos==-1 means to insert at the end. Otherwise
598                    // increment the insertion position.
599                    if (insertPos >= 0) {
600                        insertPos++;
601                    }
602
603                    // Copy all the attributes, modifying them as needed.
604                    addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER);
605                    addInnerElements(newChild, element, idMap);
606                }
607            }
608        });
609    }
610
611    // ---- Resizing ----
612
613    /** Creates a new {@link ResizeState} object to track resize state */
614    protected ResizeState createResizeState(INode layout, Object layoutView, INode node) {
615        return new ResizeState(this, layout, layoutView, node);
616    }
617
618    @Override
619    public DropFeedback onResizeBegin(@NonNull INode child, @NonNull INode parent,
620            @Nullable SegmentType horizontalEdge, @Nullable SegmentType verticalEdge,
621            @Nullable Object childView, @Nullable Object parentView) {
622        ResizeState state = createResizeState(parent, parentView, child);
623        state.horizontalEdgeType = horizontalEdge;
624        state.verticalEdgeType = verticalEdge;
625
626        // Compute preferred (wrap_content) size such that we can offer guidelines to
627        // snap to the preferred size
628        Map<INode, Rect> sizes = mRulesEngine.measureChildren(parent,
629                new IClientRulesEngine.AttributeFilter() {
630                    @Override
631                    public String getAttribute(@NonNull INode node, @Nullable String namespace,
632                            @NonNull String localName) {
633                        // Change attributes to wrap_content
634                        if (ATTR_LAYOUT_WIDTH.equals(localName)
635                                && SdkConstants.NS_RESOURCES.equals(namespace)) {
636                            return VALUE_WRAP_CONTENT;
637                        }
638                        if (ATTR_LAYOUT_HEIGHT.equals(localName)
639                                && SdkConstants.NS_RESOURCES.equals(namespace)) {
640                            return VALUE_WRAP_CONTENT;
641                        }
642
643                        return null;
644                    }
645                });
646        if (sizes != null) {
647            state.wrapBounds = sizes.get(child);
648        }
649
650        return new DropFeedback(state, new IFeedbackPainter() {
651            @Override
652            public void paint(@NonNull IGraphics gc, @NonNull INode node,
653                    @NonNull DropFeedback feedback) {
654                ResizeState resizeState = (ResizeState) feedback.userData;
655                if (resizeState != null && resizeState.bounds != null) {
656                    paintResizeFeedback(gc, node, resizeState);
657                }
658            }
659        });
660    }
661
662    protected void paintResizeFeedback(IGraphics gc, INode node, ResizeState resizeState) {
663        gc.useStyle(DrawingStyle.RESIZE_PREVIEW);
664        Rect b = resizeState.bounds;
665        gc.drawRect(b);
666
667        if (resizeState.horizontalFillSegment != null) {
668            gc.useStyle(DrawingStyle.GUIDELINE);
669            Segment s = resizeState.horizontalFillSegment;
670            gc.drawLine(s.from, s.at, s.to, s.at);
671        }
672        if (resizeState.verticalFillSegment != null) {
673            gc.useStyle(DrawingStyle.GUIDELINE);
674            Segment s = resizeState.verticalFillSegment;
675            gc.drawLine(s.at, s.from, s.at, s.to);
676        }
677
678        if (resizeState.wrapBounds != null) {
679            gc.useStyle(DrawingStyle.GUIDELINE);
680            int wrapWidth = resizeState.wrapBounds.w;
681            int wrapHeight = resizeState.wrapBounds.h;
682
683            // Show the "wrap_content" guideline.
684            // If we are showing both the wrap_width and wrap_height lines
685            // then we show at most the rectangle formed by the two lines;
686            // otherwise we show the entire width of the line
687            if (resizeState.horizontalEdgeType != null) {
688                int y = -1;
689                switch (resizeState.horizontalEdgeType) {
690                    case TOP:
691                        y = b.y + b.h - wrapHeight;
692                        break;
693                    case BOTTOM:
694                        y = b.y + wrapHeight;
695                        break;
696                    default: assert false : resizeState.horizontalEdgeType;
697                }
698                if (resizeState.verticalEdgeType != null) {
699                    switch (resizeState.verticalEdgeType) {
700                        case LEFT:
701                            gc.drawLine(b.x + b.w - wrapWidth, y, b.x + b.w, y);
702                            break;
703                        case RIGHT:
704                            gc.drawLine(b.x, y, b.x + wrapWidth, y);
705                            break;
706                        default: assert false : resizeState.verticalEdgeType;
707                    }
708                } else {
709                    gc.drawLine(b.x, y, b.x + b.w, y);
710                }
711            }
712            if (resizeState.verticalEdgeType != null) {
713                int x = -1;
714                switch (resizeState.verticalEdgeType) {
715                    case LEFT:
716                        x = b.x + b.w - wrapWidth;
717                        break;
718                    case RIGHT:
719                        x = b.x + wrapWidth;
720                        break;
721                    default: assert false : resizeState.verticalEdgeType;
722                }
723                if (resizeState.horizontalEdgeType != null) {
724                    switch (resizeState.horizontalEdgeType) {
725                        case TOP:
726                            gc.drawLine(x, b.y + b.h - wrapHeight, x, b.y + b.h);
727                            break;
728                        case BOTTOM:
729                            gc.drawLine(x, b.y, x, b.y + wrapHeight);
730                            break;
731                        default: assert false : resizeState.horizontalEdgeType;
732                    }
733                } else {
734                    gc.drawLine(x, b.y, x, b.y + b.h);
735                }
736            }
737        }
738    }
739
740    /**
741     * Returns the maximum number of pixels will be considered a "match" when snapping
742     * resize or move positions to edges or other constraints
743     *
744     * @return the maximum number of pixels to consider for snapping
745     */
746    public static final int getMaxMatchDistance() {
747        // TODO - make constant once we're happy with the feel
748        return 20;
749    }
750
751    @Override
752    public void onResizeUpdate(@Nullable DropFeedback feedback, @NonNull INode child,
753            @NonNull INode parent, @NonNull Rect newBounds, int modifierMask) {
754        ResizeState state = (ResizeState) feedback.userData;
755        state.bounds = newBounds;
756        state.modifierMask = modifierMask;
757
758        // Match on wrap bounds
759        state.wrapWidth = state.wrapHeight = false;
760        if (state.wrapBounds != null) {
761            Rect b = state.wrapBounds;
762            int maxMatchDistance = getMaxMatchDistance();
763            if (state.horizontalEdgeType != null) {
764                if (Math.abs(newBounds.h - b.h) < maxMatchDistance) {
765                    state.wrapHeight = true;
766                    if (state.horizontalEdgeType == SegmentType.TOP) {
767                        newBounds.y += newBounds.h - b.h;
768                    }
769                    newBounds.h = b.h;
770                }
771            }
772            if (state.verticalEdgeType != null) {
773                if (Math.abs(newBounds.w - b.w) < maxMatchDistance) {
774                    state.wrapWidth = true;
775                    if (state.verticalEdgeType == SegmentType.LEFT) {
776                        newBounds.x += newBounds.w - b.w;
777                    }
778                    newBounds.w = b.w;
779                }
780            }
781        }
782
783        // Match on fill bounds
784        state.horizontalFillSegment = null;
785        state.fillHeight = false;
786        if (state.horizontalEdgeType == SegmentType.BOTTOM && !state.wrapHeight) {
787            Rect parentBounds = parent.getBounds();
788            state.horizontalFillSegment = new Segment(parentBounds.y2(), newBounds.x,
789                newBounds.x2(),
790                null /*node*/, null /*id*/, SegmentType.BOTTOM, MarginType.NO_MARGIN);
791            if (Math.abs(newBounds.y2() - parentBounds.y2()) < getMaxMatchDistance()) {
792                state.fillHeight = true;
793                newBounds.h = parentBounds.y2() - newBounds.y;
794            }
795        }
796        state.verticalFillSegment = null;
797        state.fillWidth = false;
798        if (state.verticalEdgeType == SegmentType.RIGHT && !state.wrapWidth) {
799            Rect parentBounds = parent.getBounds();
800            state.verticalFillSegment = new Segment(parentBounds.x2(), newBounds.y,
801                newBounds.y2(),
802                null /*node*/, null /*id*/, SegmentType.RIGHT, MarginType.NO_MARGIN);
803            if (Math.abs(newBounds.x2() - parentBounds.x2()) < getMaxMatchDistance()) {
804                state.fillWidth = true;
805                newBounds.w = parentBounds.x2() - newBounds.x;
806            }
807        }
808
809        feedback.tooltip = getResizeUpdateMessage(state, child, parent,
810                newBounds, state.horizontalEdgeType, state.verticalEdgeType);
811    }
812
813    @Override
814    public void onResizeEnd(@Nullable DropFeedback feedback, @NonNull INode child,
815            final @NonNull INode parent, final @NonNull Rect newBounds) {
816        final Rect oldBounds = child.getBounds();
817        if (oldBounds.w != newBounds.w || oldBounds.h != newBounds.h) {
818            final ResizeState state = (ResizeState) feedback.userData;
819            child.editXml("Resize", new INodeHandler() {
820                @Override
821                public void handle(@NonNull INode n) {
822                    setNewSizeBounds(state, n, parent, oldBounds, newBounds,
823                            state.horizontalEdgeType, state.verticalEdgeType);
824                }
825            });
826        }
827    }
828
829    /**
830     * Returns the message to display to the user during the resize operation
831     *
832     * @param resizeState the current resize state
833     * @param child the child node being resized
834     * @param parent the parent of the resized node
835     * @param newBounds the new bounds to resize the child to, in pixels
836     * @param horizontalEdge the horizontal edge being resized
837     * @param verticalEdge the vertical edge being resized
838     * @return the message to display for the current resize bounds
839     */
840    protected String getResizeUpdateMessage(ResizeState resizeState, INode child, INode parent,
841            Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) {
842        String width = resizeState.getWidthAttribute();
843        String height = resizeState.getHeightAttribute();
844
845        if (horizontalEdge == null) {
846            return width;
847        } else if (verticalEdge == null) {
848            return height;
849        } else {
850            // U+00D7: Unicode for multiplication sign
851            return String.format("%s \u00D7 %s", width, height);
852        }
853    }
854
855    /**
856     * Performs the edit on the node to complete a resizing operation. The actual edit
857     * part is pulled out such that subclasses can change/add to the edits and be part of
858     * the same undo event
859     *
860     * @param resizeState the current resize state
861     * @param node the child node being resized
862     * @param layout the parent of the resized node
863     * @param newBounds the new bounds to resize the child to, in pixels
864     * @param horizontalEdge the horizontal edge being resized
865     * @param verticalEdge the vertical edge being resized
866     */
867    protected void setNewSizeBounds(ResizeState resizeState, INode node, INode layout,
868            Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) {
869        if (verticalEdge != null
870            && (newBounds.w != oldBounds.w || resizeState.wrapWidth || resizeState.fillWidth)) {
871            node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, resizeState.getWidthAttribute());
872        }
873        if (horizontalEdge != null
874            && (newBounds.h != oldBounds.h || resizeState.wrapHeight || resizeState.fillHeight)) {
875            node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, resizeState.getHeightAttribute());
876        }
877    }
878}
879