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