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