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