• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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} */
GridLayoutRule()147     public GridLayoutRule() {
148     }
149 
150     @Override
addLayoutActions(List<RuleAction> actions, final INode parentNode, final List<? extends INode> children)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      */
getCurrentOrientation(final INode node)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
onDropEnter(INode targetNode, Object targetView, IDragElement[] elements)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
onDropMove(INode targetNode, IDragElement[] elements, DropFeedback feedback, Point p)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
onDropped(final INode targetNode, final IDragElement[] elements, DropFeedback feedback, Point p)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
onChildInserted(INode node, INode parent, InsertType insertType)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      */
getNamespace(INode layout)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      */
computeDefaultGravity(FillPreference fill)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
onRemovingChildren(List<INode> deleted, INode parent)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
paintResizeFeedback(IGraphics gc, INode node, ResizeState state)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 */
getGrid(ResizeState resizeState)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
setNewSizeBounds(ResizeState state, INode node, INode layout, Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge)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
getResizeUpdateMessage(ResizeState state, INode child, INode parent, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge)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      */
resizingWidget(ResizeState state)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      */
computeResizeSpans(ResizeState state)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      */
getNewCellSize()519     public int getNewCellSize() {
520         return mRulesEngine.screenToLayout(NEW_CELL_WIDTH / 2);
521     }
522 
523     @Override
paintSelectionFeedback(IGraphics graphics, INode parentNode, List<? extends INode> childNodes, Object view)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
onPaste(INode targetNode, Object targetView, IDragElement[] elements)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