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