Home | History | Annotate | Download | only in layout
      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.
    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)
    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
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 {
    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      */
    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      */
    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      */
    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 */
    604     protected ResizeState createResizeState(INode layout, Object layoutView, INode node) {
    605         return new ResizeState(this, layout, layoutView, node);
    606     }
    607 
    608     @Override
    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