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} */ GridLayoutRule()151 public GridLayoutRule() { 152 } 153 154 @Override addLayoutActions( @onNull List<RuleAction> actions, final @NonNull INode parentNode, final @NonNull List<? extends INode> children)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 */ getCurrentOrientation(final INode node)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 onDropEnter(@onNull INode targetNode, @Nullable Object targetView, @Nullable IDragElement[] elements)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 onDropMove(@onNull INode targetNode, @NonNull IDragElement[] elements, @Nullable DropFeedback feedback, @NonNull Point p)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 onDropped(final @NonNull INode targetNode, final @NonNull IDragElement[] elements, @Nullable DropFeedback feedback, @NonNull Point p)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 onChildInserted(@onNull INode node, @NonNull INode parent, @NonNull InsertType insertType)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 */ getNamespace(INode layout)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 */ computeDefaultGravity(FillPreference fill)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 onRemovingChildren(@onNull List<INode> deleted, @NonNull INode parent, boolean moved)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 paintResizeFeedback(IGraphics gc, INode node, ResizeState state)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 */ getGrid(ResizeState resizeState)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 setNewSizeBounds(ResizeState state, INode node, INode layout, Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge)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 getResizeUpdateMessage(ResizeState state, INode child, INode parent, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge)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 */ resizingWidget(ResizeState state)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 */ computeResizeSpans(ResizeState state)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 */ getNewCellSize()581 public int getNewCellSize() { 582 return mRulesEngine.screenToLayout(NEW_CELL_WIDTH / 2); 583 } 584 585 @Override paintSelectionFeedback(@onNull IGraphics graphics, @NonNull INode parentNode, @NonNull List<? extends INode> childNodes, @Nullable Object view)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 onPaste( @onNull INode targetNode, @Nullable Object targetView, @NonNull IDragElement[] elements)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