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