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