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