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