Home | History | Annotate | Download | only in layout
      1 /*
      2  * Copyright (C) 2011 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_LAYOUT_COLUMN;
     21 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_GRAVITY;
     22 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW;
     23 import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION;
     24 import static com.android.ide.common.layout.LayoutConstants.FQCN_SPACE;
     25 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL;
     26 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_HORIZONTAL;
     27 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_VERTICAL;
     28 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_LEFT;
     29 import static com.android.ide.common.layout.LayoutConstants.VALUE_HORIZONTAL;
     30 import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE;
     31 
     32 import com.android.ide.common.api.DrawingStyle;
     33 import com.android.ide.common.api.DropFeedback;
     34 import com.android.ide.common.api.IDragElement;
     35 import com.android.ide.common.api.IFeedbackPainter;
     36 import com.android.ide.common.api.IGraphics;
     37 import com.android.ide.common.api.IMenuCallback;
     38 import com.android.ide.common.api.INode;
     39 import com.android.ide.common.api.INodeHandler;
     40 import com.android.ide.common.api.IViewMetadata;
     41 import com.android.ide.common.api.IViewMetadata.FillPreference;
     42 import com.android.ide.common.api.IViewRule;
     43 import com.android.ide.common.api.InsertType;
     44 import com.android.ide.common.api.Point;
     45 import com.android.ide.common.api.Rect;
     46 import com.android.ide.common.api.RuleAction;
     47 import com.android.ide.common.api.RuleAction.Choices;
     48 import com.android.ide.common.api.SegmentType;
     49 import com.android.ide.common.layout.grid.GridDropHandler;
     50 import com.android.ide.common.layout.grid.GridLayoutPainter;
     51 import com.android.ide.common.layout.grid.GridModel;
     52 import com.android.util.Pair;
     53 
     54 import java.net.URL;
     55 import java.util.Arrays;
     56 import java.util.Collections;
     57 import java.util.List;
     58 import java.util.Map;
     59 
     60 /**
     61  * An {@link IViewRule} for android.widget.GridLayout which provides designtime
     62  * interaction with GridLayouts.
     63  * <p>
     64  * TODO:
     65  * <ul>
     66  * <li>Handle multi-drag: preserving relative positions and alignments among dragged
     67  * views.
     68  * <li>Handle GridLayouts that have been configured in a vertical orientation.
     69  * <li>Handle free-form editing GridLayouts that have been manually edited rather than
     70  * built up using free-form editing (e.g. they might not follow the same spacing
     71  * convention, might use weights etc)
     72  * <li>Avoid setting row and column numbers on the actual elements if they can be skipped
     73  * to make the XML leaner.
     74  * </ul>
     75  */
     76 public class GridLayoutRule extends BaseLayoutRule {
     77     /**
     78      * The size of the visual regular grid that we snap to (if {@link #sSnapToGrid} is set
     79      */
     80     public static final int GRID_SIZE = 16;
     81 
     82     /** Standard gap between views */
     83     public static final int SHORT_GAP_DP = 16;
     84 
     85     /**
     86      * The preferred margin size, in pixels
     87      */
     88     public static final int MARGIN_SIZE = 32;
     89 
     90     /**
     91      * Size in screen pixels in the IDE of the gutter shown for new rows and columns (in
     92      * grid mode)
     93      */
     94     private static final int NEW_CELL_WIDTH = 10;
     95 
     96     /**
     97      * Maximum size of a widget relative to a cell which is allowed to fit into a cell
     98      * (and thereby enlarge it) before it is spread with row or column spans.
     99      */
    100     public static final double MAX_CELL_DIFFERENCE = 1.2;
    101 
    102     /** Whether debugging diagnostics is available in the toolbar */
    103     private static final boolean CAN_DEBUG =
    104             VALUE_TRUE.equals(System.getenv("ADT_DEBUG_GRIDLAYOUT")); //$NON-NLS-1$
    105 
    106     private static final String ACTION_ADD_ROW = "_addrow"; //$NON-NLS-1$
    107     private static final String ACTION_REMOVE_ROW = "_removerow"; //$NON-NLS-1$
    108     private static final String ACTION_ADD_COL = "_addcol"; //$NON-NLS-1$
    109     private static final String ACTION_REMOVE_COL = "_removecol"; //$NON-NLS-1$
    110     private static final String ACTION_ORIENTATION = "_orientation"; //$NON-NLS-1$
    111     private static final String ACTION_SHOW_STRUCTURE = "_structure"; //$NON-NLS-1$
    112     private static final String ACTION_GRID_MODE = "_gridmode"; //$NON-NLS-1$
    113     private static final String ACTION_SNAP = "_snap"; //$NON-NLS-1$
    114     private static final String ACTION_DEBUG = "_debug"; //$NON-NLS-1$
    115 
    116     private static final URL ICON_HORIZONTAL = GridLayoutRule.class.getResource("hlinear.png"); //$NON-NLS-1$
    117     private static final URL ICON_VERTICAL = GridLayoutRule.class.getResource("vlinear.png"); //$NON-NLS-1$
    118     private static final URL ICON_ADD_ROW = GridLayoutRule.class.getResource("addrow.png"); //$NON-NLS-1$
    119     private static final URL ICON_REMOVE_ROW = GridLayoutRule.class.getResource("removerow.png"); //$NON-NLS-1$
    120     private static final URL ICON_ADD_COL = GridLayoutRule.class.getResource("addcol.png"); //$NON-NLS-1$
    121     private static final URL ICON_REMOVE_COL = GridLayoutRule.class.getResource("removecol.png"); //$NON-NLS-1$
    122     private static final URL ICON_SHOW_STRUCT = GridLayoutRule.class.getResource("showgrid.png"); //$NON-NLS-1$
    123     private static final URL ICON_GRID_MODE = GridLayoutRule.class.getResource("gridmode.png"); //$NON-NLS-1$
    124     private static final URL ICON_SNAP = GridLayoutRule.class.getResource("snap.png"); //$NON-NLS-1$
    125 
    126     /**
    127      * Whether the IDE should show diagnostics for debugging the grid layout - including
    128      * spacers visibly in the outline, showing row and column numbers, and so on
    129      */
    130     public static boolean sDebugGridLayout = CAN_DEBUG;
    131 
    132     /** Whether the structure (grid model) should be displayed persistently to the user */
    133     public static boolean sShowStructure = false;
    134 
    135     /** Whether the drop positions should snap to a regular grid */
    136     public static boolean sSnapToGrid = false;
    137 
    138     /**
    139      * Whether the grid is edited in "grid mode" where the operations are row/column based
    140      * rather than free-form
    141      */
    142     public static boolean sGridMode = false;
    143 
    144     /** Constructs a new {@link GridLayoutRule} */
    145     public GridLayoutRule() {
    146     }
    147 
    148     @Override
    149     public void addLayoutActions(List<RuleAction> actions, final INode parentNode,
    150             final List<? extends INode> children) {
    151         super.addLayoutActions(actions, parentNode, children);
    152 
    153         Choices orientationAction = RuleAction.createChoices(
    154                 ACTION_ORIENTATION,
    155                 "Orientation", //$NON-NLS-1$
    156                 new PropertyCallback(Collections.singletonList(parentNode),
    157                         "Change LinearLayout Orientation", ANDROID_URI, ATTR_ORIENTATION), Arrays
    158                         .<String> asList("Set Horizontal Orientation", "Set Vertical Orientation"),
    159                 Arrays.<URL> asList(ICON_HORIZONTAL, ICON_VERTICAL), Arrays.<String> asList(
    160                         "horizontal", "vertical"), getCurrentOrientation(parentNode),
    161                 null /* icon */, -10, false);
    162         orientationAction.setRadio(true);
    163         actions.add(orientationAction);
    164 
    165         // Gravity and margins
    166         if (children != null && children.size() > 0) {
    167             actions.add(RuleAction.createSeparator(35));
    168             actions.add(createMarginAction(parentNode, children));
    169             actions.add(createGravityAction(children, ATTR_LAYOUT_GRAVITY));
    170         }
    171 
    172         IMenuCallback actionCallback = new IMenuCallback() {
    173             public void action(final RuleAction action, List<? extends INode> selectedNodes,
    174                     final String valueId, final Boolean newValue) {
    175                 parentNode.editXml("Add/Remove Row/Column", new INodeHandler() {
    176                     public void handle(INode n) {
    177                         String id = action.getId();
    178                         if (id.equals(ACTION_SHOW_STRUCTURE)) {
    179                             sShowStructure = !sShowStructure;
    180                             mRulesEngine.redraw();
    181                             return;
    182                         } else if (id.equals(ACTION_GRID_MODE)) {
    183                             sGridMode = !sGridMode;
    184                             mRulesEngine.redraw();
    185                             return;
    186                         } else if (id.equals(ACTION_SNAP)) {
    187                             sSnapToGrid = !sSnapToGrid;
    188                             mRulesEngine.redraw();
    189                             return;
    190                         } else if (id.equals(ACTION_DEBUG)) {
    191                             sDebugGridLayout = !sDebugGridLayout;
    192                             mRulesEngine.layout();
    193                             return;
    194                         }
    195 
    196                         GridModel grid = new GridModel(mRulesEngine, parentNode, null);
    197                         if (id.equals(ACTION_ADD_ROW)) {
    198                             grid.addRow(children);
    199                         } else if (id.equals(ACTION_REMOVE_ROW)) {
    200                             grid.removeRows(children);
    201                         } else if (id.equals(ACTION_ADD_COL)) {
    202                             grid.addColumn(children);
    203                         } else if (id.equals(ACTION_REMOVE_COL)) {
    204                             grid.removeColumns(children);
    205                         }
    206                     }
    207 
    208                 });
    209             }
    210         };
    211 
    212         actions.add(RuleAction.createSeparator(142));
    213 
    214         actions.add(RuleAction.createToggle(ACTION_GRID_MODE, "Grid Model Mode",
    215                 sGridMode, actionCallback, ICON_GRID_MODE, 145, false));
    216 
    217         // Add and Remove Column actions only apply in Grid Mode
    218         if (sGridMode) {
    219             // Add Row and Add Column
    220             actions.add(RuleAction.createSeparator(150));
    221             actions.add(RuleAction.createAction(ACTION_ADD_COL, "Add Column", actionCallback,
    222                     ICON_ADD_COL, 160, false /* supportsMultipleNodes */));
    223             actions.add(RuleAction.createAction(ACTION_ADD_ROW, "Add Row", actionCallback,
    224                     ICON_ADD_ROW, 165, false));
    225 
    226             // Remove Row and Remove Column (if something is selected)
    227             if (children != null && children.size() > 0) {
    228                 // TODO: Add "Merge Columns" and "Merge Rows" ?
    229 
    230                 actions.add(RuleAction.createAction(ACTION_REMOVE_COL, "Remove Column",
    231                         actionCallback, ICON_REMOVE_COL, 170, false));
    232                 actions.add(RuleAction.createAction(ACTION_REMOVE_ROW, "Remove Row",
    233                         actionCallback, ICON_REMOVE_ROW, 175, false));
    234             }
    235 
    236             actions.add(RuleAction.createSeparator(185));
    237         } else {
    238             actions.add(RuleAction.createToggle(ACTION_SHOW_STRUCTURE, "Show Structure",
    239                     sShowStructure, actionCallback, ICON_SHOW_STRUCT, 190, false));
    240 
    241             // Snap to Grid and Show Structure are only relevant in free form mode
    242             actions.add(RuleAction.createToggle(ACTION_SNAP, "Snap to Grid",
    243                     sSnapToGrid, actionCallback, ICON_SNAP, 200, false));
    244         }
    245 
    246         // Temporary: Diagnostics for GridLayout
    247         if (CAN_DEBUG) {
    248             actions.add(RuleAction.createToggle(ACTION_DEBUG, "Debug",
    249                     sDebugGridLayout, actionCallback, null, 210, false));
    250         }
    251     }
    252 
    253     /**
    254      * Returns the orientation attribute value currently used by the node (even if not
    255      * defined, in which case the default horizontal value is returned)
    256      */
    257     private static String getCurrentOrientation(final INode node) {
    258         String orientation = node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION);
    259         if (orientation == null || orientation.length() == 0) {
    260             orientation = VALUE_HORIZONTAL;
    261         }
    262         return orientation;
    263     }
    264 
    265     @Override
    266     public DropFeedback onDropEnter(INode targetNode, Object targetView, IDragElement[] elements) {
    267         GridDropHandler userData = new GridDropHandler(this, targetNode, targetView);
    268         IFeedbackPainter painter = GridLayoutPainter.createDropFeedbackPainter(this, elements);
    269         return new DropFeedback(userData, painter);
    270     }
    271 
    272     @Override
    273     public DropFeedback onDropMove(INode targetNode, IDragElement[] elements,
    274             DropFeedback feedback, Point p) {
    275         feedback.requestPaint = true;
    276 
    277         GridDropHandler handler = (GridDropHandler) feedback.userData;
    278         handler.computeMatches(feedback, p);
    279 
    280         return feedback;
    281     }
    282 
    283     @Override
    284     public void onDropped(final INode targetNode, final IDragElement[] elements,
    285             DropFeedback feedback, Point p) {
    286         Rect b = targetNode.getBounds();
    287         if (!b.isValid()) {
    288             return;
    289         }
    290 
    291         GridDropHandler dropHandler = (GridDropHandler) feedback.userData;
    292         if (dropHandler.getRowMatch() == null || dropHandler.getColumnMatch() == null) {
    293             return;
    294         }
    295 
    296         // Collect IDs from dropped elements and remap them to new IDs
    297         // if this is a copy or from a different canvas.
    298         Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements,
    299                 feedback.isCopy || !feedback.sameCanvas);
    300 
    301         for (IDragElement element : elements) {
    302             INode newChild;
    303             if (!sGridMode) {
    304                 newChild = dropHandler.handleFreeFormDrop(targetNode, element);
    305             } else {
    306                 newChild = dropHandler.handleGridModeDrop(targetNode, element);
    307             }
    308 
    309             // Copy all the attributes, modifying them as needed.
    310             addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER);
    311 
    312             addInnerElements(newChild, element, idMap);
    313         }
    314     }
    315 
    316     @Override
    317     public void onChildInserted(INode node, INode parent, InsertType insertType) {
    318         if (insertType == InsertType.MOVE_WITHIN) {
    319             // Don't adjust widths/heights/weights when just moving within a single layout
    320             return;
    321         }
    322 
    323         // Attempt to set "fill" properties on newly added views such that for example
    324         // a text field will stretch horizontally.
    325         String fqcn = node.getFqcn();
    326         IViewMetadata metadata = mRulesEngine.getMetadata(fqcn);
    327         if (metadata == null) {
    328             return;
    329         }
    330         FillPreference fill = metadata.getFillPreference();
    331         String gravity = computeDefaultGravity(fill);
    332         if (gravity != null) {
    333             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_GRAVITY, gravity);
    334         }
    335     }
    336 
    337     /**
    338      * Computes the default gravity to be used for a widget of the given fill
    339      * preference when added to a grid layout
    340      *
    341      * @param fill the fill preference for the widget
    342      * @return the gravity value, or null, to be set on the widget
    343      */
    344     public static String computeDefaultGravity(FillPreference fill) {
    345         String horizontal = GRAVITY_VALUE_LEFT;
    346         String vertical = null;
    347         if (fill.fillHorizontally(true /*verticalContext*/)) {
    348             horizontal = GRAVITY_VALUE_FILL_HORIZONTAL;
    349         }
    350         if (fill.fillVertically(true /*verticalContext*/)) {
    351             vertical = GRAVITY_VALUE_FILL_VERTICAL;
    352         }
    353         String gravity;
    354         if (horizontal == GRAVITY_VALUE_FILL_HORIZONTAL
    355                 && vertical == GRAVITY_VALUE_FILL_VERTICAL) {
    356             gravity = GRAVITY_VALUE_FILL;
    357         } else if (vertical != null) {
    358             gravity = horizontal + '|' + vertical;
    359         } else {
    360             gravity = horizontal;
    361         }
    362 
    363         return gravity;
    364     }
    365 
    366     @Override
    367     public void onRemovingChildren(List<INode> deleted, INode parent) {
    368         super.onRemovingChildren(deleted, parent);
    369 
    370         // Attempt to clean up spacer objects for any newly-empty rows or columns
    371         // as the result of this deletion
    372         GridModel grid = new GridModel(mRulesEngine, parent, null);
    373         for (INode child : deleted) {
    374             // We don't care about deletion of spacers
    375             if (child.getFqcn().equals(FQCN_SPACE)) {
    376                 continue;
    377             }
    378             grid.markDeleted(child);
    379         }
    380 
    381         grid.cleanup();
    382     }
    383 
    384     @Override
    385     protected void paintResizeFeedback(IGraphics gc, INode node, ResizeState state) {
    386         if (!sGridMode) {
    387             GridModel grid = getGrid(state);
    388             GridLayoutPainter.paintResizeFeedback(gc, state.layout, grid);
    389         }
    390 
    391         if (resizingWidget(state)) {
    392             super.paintResizeFeedback(gc, node, state);
    393         } else {
    394             GridModel grid = getGrid(state);
    395             int startColumn = grid.getColumn(state.bounds.x);
    396             int endColumn = grid.getColumn(state.bounds.x2());
    397             int columnSpan = endColumn - startColumn + 1;
    398 
    399             int startRow = grid.getRow(state.bounds.y);
    400             int endRow = grid.getRow(state.bounds.y2());
    401             int rowSpan = endRow - startRow + 1;
    402 
    403             Rect cellBounds = grid.getCellBounds(startRow, startColumn, rowSpan, columnSpan);
    404             gc.useStyle(DrawingStyle.RESIZE_PREVIEW);
    405             gc.drawRect(cellBounds);
    406         }
    407     }
    408 
    409     /** Returns the grid size cached on the given {@link ResizeState} object */
    410     private GridModel getGrid(ResizeState resizeState) {
    411         GridModel grid = (GridModel) resizeState.clientData;
    412         if (grid == null) {
    413             grid = new GridModel(mRulesEngine, resizeState.layout, resizeState.layoutView);
    414             resizeState.clientData = grid;
    415         }
    416 
    417         return grid;
    418     }
    419 
    420     @Override
    421     protected void setNewSizeBounds(ResizeState state, INode node, INode layout,
    422             Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) {
    423 
    424         if (resizingWidget(state)) {
    425             super.setNewSizeBounds(state, node, layout, oldBounds, newBounds, horizontalEdge,
    426                     verticalEdge);
    427         } else {
    428             Pair<Integer, Integer> spans = computeResizeSpans(state);
    429             int rowSpan = spans.getFirst();
    430             int columnSpan = spans.getSecond();
    431             GridModel.setColumnSpanAttribute(node, columnSpan);
    432             GridModel.setRowSpanAttribute(node, rowSpan);
    433         }
    434     }
    435 
    436     @Override
    437     protected String getResizeUpdateMessage(ResizeState state, INode child, INode parent,
    438             Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) {
    439         Pair<Integer, Integer> spans = computeResizeSpans(state);
    440         if (resizingWidget(state)) {
    441             String width = state.getWidthAttribute();
    442             String height = state.getHeightAttribute();
    443 
    444             String message;
    445             if (horizontalEdge == null) {
    446                 message = width;
    447             } else if (verticalEdge == null) {
    448                 message = height;
    449             } else {
    450                 // U+00D7: Unicode for multiplication sign
    451                 message = String.format("%s \u00D7 %s", width, height);
    452             }
    453 
    454             // Tack on a tip about using the Shift modifier key
    455             return String.format("%s\n(Press Shift to resize row/column spans)", message);
    456         } else {
    457             int rowSpan = spans.getFirst();
    458             int columnSpan = spans.getSecond();
    459             return String.format("ColumnSpan=%d, RowSpan=%d\n(Release Shift to resize widget itself)",
    460                     columnSpan, rowSpan);
    461         }
    462     }
    463 
    464     /**
    465      * Returns true if we're resizing the widget, and false if we're resizing the cell
    466      * spans
    467      */
    468     private static boolean resizingWidget(ResizeState state) {
    469         return (state.modifierMask & DropFeedback.MODIFIER2) == 0;
    470     }
    471 
    472     /**
    473      * Computes the new column and row spans as the result of the current resizing
    474      * operation
    475      */
    476     private Pair<Integer, Integer> computeResizeSpans(ResizeState state) {
    477         GridModel grid = getGrid(state);
    478 
    479         int startColumn = grid.getColumn(state.bounds.x);
    480         int endColumn = grid.getColumn(state.bounds.x2());
    481         int columnSpan = endColumn - startColumn + 1;
    482 
    483         int startRow = grid.getRow(state.bounds.y);
    484         int endRow = grid.getRow(state.bounds.y2());
    485         int rowSpan = endRow - startRow + 1;
    486 
    487         return Pair.of(rowSpan, columnSpan);
    488     }
    489 
    490     /**
    491      * Returns the size of the new cell gutter in layout coordinates
    492      *
    493      * @return the size of the new cell gutter in layout coordinates
    494      */
    495     public int getNewCellSize() {
    496         return mRulesEngine.screenToLayout(NEW_CELL_WIDTH / 2);
    497     }
    498 
    499     @Override
    500     public void paintSelectionFeedback(IGraphics graphics, INode parentNode,
    501             List<? extends INode> childNodes, Object view) {
    502         super.paintSelectionFeedback(graphics, parentNode, childNodes, view);
    503 
    504         if (sShowStructure) {
    505             // TODO: Cache the grid
    506             if (view != null) {
    507                 if (GridLayoutPainter.paintStructure(view, DrawingStyle.GUIDELINE_DASHED,
    508                         parentNode, graphics)) {
    509                     return;
    510                 }
    511             }
    512             GridLayoutPainter.paintStructure(DrawingStyle.GUIDELINE_DASHED,
    513                         parentNode, graphics, new GridModel(mRulesEngine, parentNode, view));
    514         } else if (sDebugGridLayout) {
    515             GridLayoutPainter.paintStructure(DrawingStyle.GRID,
    516                     parentNode, graphics, new GridModel(mRulesEngine, parentNode, view));
    517         }
    518 
    519         // TBD: Highlight the cells around the selection, and display easy controls
    520         // for for example tweaking the rowspan/colspan of a cell? (but only in grid mode)
    521     }
    522 
    523     /**
    524      * Paste into a GridLayout. We have several possible behaviors (and many
    525      * more than are listed here):
    526      * <ol>
    527      * <li> Preserve the current positions of the elements (if pasted from another
    528      *      canvas, not just XML markup copied from say a web site) and apply those
    529      *      into the current grid. This might mean "overwriting" (sitting on top of)
    530      *      existing elements.
    531      * <li> Fill available "holes" in the grid.
    532      * <li> Lay them out consecutively, row by row, like text.
    533      * <li> Some hybrid approach, where I attempt to preserve the <b>relative</b>
    534      *      relationships (columns/wrapping, spacing between the pasted views etc)
    535      *      but I append them to the bottom of the layout on one or more new rows.
    536      * <li> Try to paste at the current mouse position, if known, preserving the
    537      *      relative distances between the existing elements there.
    538      * </ol>
    539      * Attempting to preserve the current position isn't possible right now,
    540      * because the clipboard data contains only the textual representation of
    541      * the markup. (We'd need to stash position information from a previous
    542      * layout render along with the clipboard data).
    543      * <p>
    544      * Currently, this implementation simply lays out the elements row by row,
    545      * approach #3 above.
    546      */
    547     @Override
    548     public void onPaste(INode targetNode, Object targetView, IDragElement[] elements) {
    549         DropFeedback feedback = onDropEnter(targetNode, targetView, elements);
    550         if (feedback != null) {
    551             Rect b = targetNode.getBounds();
    552             if (!b.isValid()) {
    553                 return;
    554             }
    555 
    556             Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements,
    557                     true /* remap id's */);
    558 
    559             for (IDragElement element : elements) {
    560                 // Skip <Space> elements and only insert the real elements being
    561                 // copied
    562                 if (elements.length > 1 && FQCN_SPACE.equals(element.getFqcn())) {
    563                     continue;
    564                 }
    565 
    566                 String fqcn = element.getFqcn();
    567                 INode newChild = targetNode.appendChild(fqcn);
    568                 addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER);
    569 
    570                 // Ensure that we reset any potential row/column attributes from a different
    571                 // grid layout being copied from
    572                 newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN, null);
    573                 newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, null);
    574 
    575                 // TODO: Set columnSpans to avoid making these widgets completely
    576                 // break the layout
    577                 // Alternatively, I could just lay them all out on subsequent lines
    578                 // with a column span of columnSpan5
    579 
    580                 addInnerElements(newChild, element, idMap);
    581             }
    582         }
    583     }
    584 }
    585