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