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.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.
    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)
    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
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 {
    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      */
    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      */
    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      */
    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 */
    628     protected ResizeState createResizeState(INode layout, Object layoutView, INode node) {
    629         return new ResizeState(this, layout, layoutView, node);
    630     }
    631 
    632     @Override
    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