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 package com.android.ide.common.layout.grid; 17 18 import static com.android.ide.common.layout.GridLayoutRule.GRID_SIZE; 19 import static com.android.ide.common.layout.GridLayoutRule.MARGIN_SIZE; 20 import static com.android.ide.common.layout.GridLayoutRule.MAX_CELL_DIFFERENCE; 21 import static com.android.ide.common.layout.GridLayoutRule.SHORT_GAP_DP; 22 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; 23 import static com.android.ide.common.layout.LayoutConstants.ATTR_COLUMN_COUNT; 24 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN; 25 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN_SPAN; 26 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_GRAVITY; 27 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW; 28 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW_SPAN; 29 import static com.android.ide.common.layout.LayoutConstants.VALUE_1; 30 import static com.android.ide.common.layout.LayoutConstants.VALUE_BOTTOM; 31 import static com.android.ide.common.layout.LayoutConstants.VALUE_CENTER_HORIZONTAL; 32 import static com.android.ide.common.layout.LayoutConstants.VALUE_RIGHT; 33 import static com.android.ide.common.layout.grid.GridModel.UNDEFINED; 34 import static java.lang.Math.abs; 35 36 import com.android.ide.common.api.DropFeedback; 37 import com.android.ide.common.api.IDragElement; 38 import com.android.ide.common.api.INode; 39 import com.android.ide.common.api.IViewMetadata; 40 import com.android.ide.common.api.Margins; 41 import com.android.ide.common.api.Point; 42 import com.android.ide.common.api.Rect; 43 import com.android.ide.common.api.SegmentType; 44 import com.android.ide.common.layout.BaseLayoutRule; 45 import com.android.ide.common.layout.GridLayoutRule; 46 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository; 47 48 import java.util.ArrayList; 49 import java.util.Collection; 50 import java.util.Collections; 51 import java.util.List; 52 53 /** 54 * The {@link GridDropHandler} handles drag and drop operations into and within a 55 * GridLayout, computing guidelines, handling drops to edit the grid model, and so on. 56 */ 57 public class GridDropHandler { 58 private final GridModel mGrid; 59 private final GridLayoutRule mRule; 60 private GridMatch mColumnMatch; 61 private GridMatch mRowMatch; 62 63 /** 64 * Creates a new {@link GridDropHandler} for 65 * @param gridLayoutRule the corresponding {@link GridLayoutRule} 66 * @param layout the GridLayout node 67 * @param view the view instance of the grid layout receiving the drop 68 */ GridDropHandler(GridLayoutRule gridLayoutRule, INode layout, Object view)69 public GridDropHandler(GridLayoutRule gridLayoutRule, INode layout, Object view) { 70 mRule = gridLayoutRule; 71 mGrid = new GridModel(mRule.getRulesEngine(), layout, view); 72 } 73 74 /** 75 * Computes the best horizontal and vertical matches for a drag to the given position. 76 * 77 * @param feedback a {@link DropFeedback} object containing drag state like the drag 78 * bounds and the drag baseline 79 * @param p the mouse position 80 */ computeMatches(DropFeedback feedback, Point p)81 public void computeMatches(DropFeedback feedback, Point p) { 82 mRowMatch = mColumnMatch = null; 83 84 Rect bounds = mGrid.layout.getBounds(); 85 int x1 = p.x; 86 int y1 = p.y; 87 88 if (!GridLayoutRule.sGridMode) { 89 Rect dragBounds = feedback.dragBounds; 90 if (dragBounds != null) { 91 // Sometimes the items are centered under the mouse so 92 // offset by the top left corner distance 93 x1 += dragBounds.x; 94 y1 += dragBounds.y; 95 } 96 97 int w = dragBounds != null ? dragBounds.w : 0; 98 int h = dragBounds != null ? dragBounds.h : 0; 99 int x2 = x1 + w; 100 int y2 = y1 + h; 101 102 if (x2 < bounds.x || y2 < bounds.y || x1 > bounds.x2() || y1 > bounds.y2()) { 103 return; 104 } 105 106 List<GridMatch> columnMatches = new ArrayList<GridMatch>(); 107 List<GridMatch> rowMatches = new ArrayList<GridMatch>(); 108 int max = BaseLayoutRule.getMaxMatchDistance(); 109 110 // Column matches: 111 addLeftSideMatch(x1, columnMatches, max); 112 addRightSideMatch(x2, columnMatches, max); 113 addCenterColumnMatch(bounds, x1, y1, x2, y2, columnMatches, max); 114 115 // Row matches: 116 int row = mGrid.getClosestRow(y1); 117 int rowY = mGrid.getRowY(row); 118 addTopMatch(y1, rowMatches, max, row, rowY); 119 addBaselineMatch(feedback.dragBaseline, y1, rowMatches, max, row, rowY); 120 addBottomMatch(y2, rowMatches, max); 121 122 // Look for gap-matches: Predefined spacing between widgets. 123 // TODO: Make this use metadata for predefined spacing between 124 // pairs of types of components. For example, buttons have certain 125 // inserts in their 9-patch files (depending on the theme) that should 126 // be considered and subtracted from the overall proposed distance! 127 addColumnGapMatch(bounds, x1, x2, columnMatches, max); 128 addRowGapMatch(bounds, y1, y2, rowMatches, max); 129 130 // Fallback: Split existing cell. Also do snap-to-grid. 131 if (GridLayoutRule.sSnapToGrid) { 132 x1 = ((x1 - MARGIN_SIZE - bounds.x) / GRID_SIZE) * GRID_SIZE 133 + MARGIN_SIZE + bounds.x; 134 y1 = ((y1 - MARGIN_SIZE - bounds.y) / GRID_SIZE) * GRID_SIZE 135 + MARGIN_SIZE + bounds.y; 136 x2 = x1 + w; 137 y2 = y1 + h; 138 } 139 140 141 if (columnMatches.size() == 0) { 142 // Split the current cell since we have no matches 143 // TODO: Decide whether it should be gravity left or right... 144 columnMatches.add(new GridMatch(SegmentType.LEFT, 0, x1, mGrid.getColumn(x1), 145 true /* createCell */, UNDEFINED)); 146 } 147 if (rowMatches.size() == 0) { 148 rowMatches.add(new GridMatch(SegmentType.TOP, 0, y1, mGrid.getRow(y1), 149 true /* createCell */, UNDEFINED)); 150 } 151 152 // Pick best matches 153 Collections.sort(rowMatches); 154 Collections.sort(columnMatches); 155 156 mColumnMatch = null; 157 mRowMatch = null; 158 String columnDescription = null; 159 String rowDescription = null; 160 if (columnMatches.size() > 0) { 161 mColumnMatch = columnMatches.get(0); 162 columnDescription = mColumnMatch.getDisplayName(mGrid.layout); 163 } 164 if (rowMatches.size() > 0) { 165 mRowMatch = rowMatches.get(0); 166 rowDescription = mRowMatch.getDisplayName(mGrid.layout); 167 } 168 if (columnDescription != null) { 169 if (rowDescription != null) { 170 feedback.tooltip = columnDescription + '\n' + rowDescription; 171 } else { 172 feedback.tooltip = columnDescription; 173 } 174 } else if (rowDescription != null) { 175 feedback.tooltip = rowDescription; 176 } else { 177 feedback.tooltip = null; 178 } 179 180 feedback.invalidTarget = mColumnMatch == null || mRowMatch == null; 181 } else { 182 // Find which cell we're inside. 183 184 // TODO: Find out where within the cell we are, and offer to tweak the gravity 185 // based on the position. 186 int column = mGrid.getColumn(x1); 187 int row = mGrid.getRow(y1); 188 189 int leftDistance = mGrid.getColumnDistance(column, x1); 190 int rightDistance = mGrid.getColumnDistance(column + 1, x1); 191 int topDistance = mGrid.getRowDistance(row, y1); 192 int bottomDistance = mGrid.getRowDistance(row + 1, y1); 193 194 int SLOP = 2; 195 int radius = mRule.getNewCellSize(); 196 if (rightDistance < radius + SLOP) { 197 column++; 198 leftDistance = rightDistance; 199 } 200 if (bottomDistance < radius + SLOP) { 201 row++; 202 topDistance = bottomDistance; 203 } 204 205 boolean matchLeft = leftDistance < radius + SLOP; 206 boolean matchTop = topDistance < radius + SLOP; 207 208 mColumnMatch = new GridMatch(SegmentType.LEFT, 0, x1, column, matchLeft, 0); 209 mRowMatch = new GridMatch(SegmentType.TOP, 0, y1, row, matchTop, 0); 210 } 211 } 212 213 /** 214 * Adds a match to align the left edge with some other edge. 215 */ 216 private void addLeftSideMatch(int x1, List<GridMatch> columnMatches, int max) { 217 int column = mGrid.getClosestColumn(x1); 218 int columnX = mGrid.getColumnX(column); 219 int distance = abs(columnX - x1); 220 if (distance <= max) { 221 columnMatches.add(new GridMatch(SegmentType.LEFT, distance, columnX, column, 222 false, UNDEFINED)); 223 } 224 } 225 226 /** 227 * Adds a match to align the right edge with some other edge. 228 */ 229 private void addRightSideMatch(int x2, List<GridMatch> columnMatches, int max) { 230 // TODO: Only match the right hand side if the drag bounds fit fully within the 231 // cell! Ditto for match below. 232 int columnRight = mGrid.getClosestColumn(x2); 233 int rightDistance = mGrid.getColumnDistance(columnRight, x2); 234 if (rightDistance < max) { 235 columnMatches.add(new GridMatch(SegmentType.RIGHT, rightDistance, 236 mGrid.getColumnX(columnRight), columnRight, false, UNDEFINED)); 237 } 238 } 239 240 /** 241 * Adds a horizontal match with the center axis of the GridLayout 242 */ 243 private void addCenterColumnMatch(Rect bounds, int x1, int y1, int x2, int y2, 244 List<GridMatch> columnMatches, int max) { 245 Collection<INode> intersectsRow = mGrid.getIntersectsRow(y1, y2); 246 if (intersectsRow.size() == 0) { 247 // Offer centering on this row since there isn't anything there 248 int matchedLine = bounds.centerX(); 249 int distance = abs((x1 + x2) / 2 - matchedLine); 250 if (distance <= 2 * max) { 251 boolean createCell = false; // always just put in column 0 252 columnMatches.add(new GridMatch(SegmentType.CENTER_HORIZONTAL, distance, 253 matchedLine, 0 /* column */, createCell, UNDEFINED)); 254 } 255 } 256 } 257 258 /** 259 * Adds a match to align the top edge with some other edge. 260 */ 261 private void addTopMatch(int y1, List<GridMatch> rowMatches, int max, int row, int rowY) { 262 int distance = mGrid.getRowDistance(row, y1); 263 if (distance <= max) { 264 rowMatches.add(new GridMatch(SegmentType.TOP, distance, rowY, row, false, 265 UNDEFINED)); 266 } 267 } 268 269 /** 270 * Adds a match to align the bottom edge with some other edge. 271 */ 272 private void addBottomMatch(int y2, List<GridMatch> rowMatches, int max) { 273 int rowBottom = mGrid.getClosestRow(y2); 274 int distance = mGrid.getRowDistance(rowBottom, y2); 275 if (distance < max) { 276 int rowY = mGrid.getRowY(rowBottom); 277 rowMatches.add(new GridMatch(SegmentType.BOTTOM, distance, rowY, 278 rowBottom, false, UNDEFINED)); 279 } 280 } 281 282 /** 283 * Adds a baseline match, if applicable. 284 */ 285 private void addBaselineMatch(int dragBaseline, int y1, List<GridMatch> rowMatches, int max, 286 int row, int rowY) { 287 int dragBaselineY = y1 + dragBaseline; 288 int rowBaseline = mGrid.getBaseline(row); 289 if (rowBaseline != -1) { 290 int rowBaselineY = rowY + rowBaseline; 291 int distance = abs(dragBaselineY - rowBaselineY); 292 if (distance < max) { 293 rowMatches.add(new GridMatch(SegmentType.BASELINE, distance, rowBaselineY, row, 294 false, UNDEFINED)); 295 } 296 } 297 } 298 299 /** 300 * Computes a horizontal "gap" match - a preferred distance from the nearest edge, 301 * including margin edges 302 */ 303 private void addColumnGapMatch(Rect bounds, int x1, int x2, List<GridMatch> columnMatches, 304 int max) { 305 if (x1 < bounds.x + MARGIN_SIZE + max) { 306 int matchedLine = bounds.x + MARGIN_SIZE; 307 int distance = abs(matchedLine - x1); 308 if (distance <= max) { 309 boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; 310 columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine, 311 0, createCell, MARGIN_SIZE)); 312 } 313 } else if (x2 > bounds.x2() - MARGIN_SIZE - max) { 314 int matchedLine = bounds.x2() - MARGIN_SIZE; 315 int distance = abs(matchedLine - x2); 316 if (distance <= max) { 317 // This does not yet work properly; we need to use columnWeights to achieve this 318 //boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; 319 //columnMatches.add(new GridMatch(SegmentType.RIGHT, distance, matchedLine, 320 // mGrid.actualColumnCount - 1, createCell, MARGIN_SIZE)); 321 } 322 } else { 323 int columnRight = mGrid.getColumn(x1 - SHORT_GAP_DP); 324 int columnX = mGrid.getColumnMaxX(columnRight); 325 int matchedLine = columnX + SHORT_GAP_DP; 326 int distance = abs(matchedLine - x1); 327 if (distance <= max) { 328 boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; 329 columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine, 330 columnRight, createCell, SHORT_GAP_DP)); 331 } 332 333 // Add a column directly adjacent (no gap) 334 columnRight = mGrid.getColumn(x1); 335 columnX = mGrid.getColumnMaxX(columnRight); 336 matchedLine = columnX; 337 distance = abs(matchedLine - x1); 338 339 // Let's say you have this arrangement: 340 // [button1][button2] 341 // This is two columns, where the right hand side edge of column 1 is 342 // flush with the left side edge of column 2, because in fact the width of 343 // button1 is what defines the width of column 1, and that in turn is what 344 // defines the left side position of column 2. 345 // 346 // In this case we don't want to consider inserting a new column at the 347 // right hand side of button1 a better match than matching left on column 2. 348 // Therefore, to ensure that this doesn't happen, we "penalize" right column 349 // matches such that they don't get preferential treatment when the matching 350 // line is on the left side of the column. 351 distance += 2; 352 353 if (distance <= max) { 354 boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; 355 columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine, 356 columnRight, createCell, 0)); 357 } 358 } 359 } 360 361 /** 362 * Computes a vertical "gap" match - a preferred distance from the nearest edge, 363 * including margin edges 364 */ 365 private void addRowGapMatch(Rect bounds, int y1, int y2, List<GridMatch> rowMatches, int max) { 366 if (y1 < bounds.y + MARGIN_SIZE + max) { 367 int matchedLine = bounds.y + MARGIN_SIZE; 368 int distance = abs(matchedLine - y1); 369 if (distance <= max) { 370 boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; 371 rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine, 372 0, createCell, MARGIN_SIZE)); 373 } 374 } else if (y2 > bounds.y2() - MARGIN_SIZE - max) { 375 int matchedLine = bounds.y2() - MARGIN_SIZE; 376 int distance = abs(matchedLine - y2); 377 if (distance <= max) { 378 // This does not yet work properly; we need to use columnWeights to achieve this 379 //boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; 380 //rowMatches.add(new GridMatch(SegmentType.BOTTOM, distance, matchedLine, 381 // mGrid.actualRowCount - 1, createCell, MARGIN_SIZE)); 382 } 383 } else { 384 int rowBottom = mGrid.getRow(y1 - SHORT_GAP_DP); 385 int rowY = mGrid.getRowMaxY(rowBottom); 386 int matchedLine = rowY + SHORT_GAP_DP; 387 int distance = abs(matchedLine - y1); 388 if (distance <= max) { 389 boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; 390 rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine, 391 rowBottom, createCell, SHORT_GAP_DP)); 392 } 393 394 // Add a row directly adjacent (no gap) 395 rowBottom = mGrid.getRow(y1); 396 rowY = mGrid.getRowMaxY(rowBottom); 397 matchedLine = rowY; 398 distance = abs(matchedLine - y1); 399 distance += 2; // See explanation in addColumnGapMatch 400 if (distance <= max) { 401 boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; 402 rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine, 403 rowBottom, createCell, 0)); 404 } 405 406 } 407 } 408 409 /** 410 * Called when a node is dropped in free-form mode. This will insert the dragged 411 * element into the grid and returns the newly created node. 412 * 413 * @param targetNode the GridLayout node 414 * @param element the dragged element 415 * @return the newly created {@link INode} 416 */ 417 public INode handleFreeFormDrop(INode targetNode, IDragElement element) { 418 assert mRowMatch != null; 419 assert mColumnMatch != null; 420 421 String fqcn = element.getFqcn(); 422 423 INode newChild = null; 424 425 Rect bounds = element.getBounds(); 426 int row = mRowMatch.cellIndex; 427 int column = mColumnMatch.cellIndex; 428 429 if (targetNode.getChildren().length == 0) { 430 // 431 // Set up the initial structure: 432 // 433 // 434 // Fixed Fixed 435 // Size Size 436 // Column Expanding Column Column 437 // +-----+-------------------------------+-----+ 438 // | | | | 439 // | 0,0 | 0,1 | 0,2 | Fixed Size Row 440 // | | | | 441 // +-----+-------------------------------+-----+ 442 // | | | | 443 // | | | | 444 // | | | | 445 // | 1,0 | 1,1 | 1,2 | Expanding Row 446 // | | | | 447 // | | | | 448 // | | | | 449 // +-----+-------------------------------+-----+ 450 // | | | | 451 // | 2,0 | 2,1 | 2,2 | Fixed Size Row 452 // | | | | 453 // +-----+-------------------------------+-----+ 454 // 455 // This is implemented in GridLayout by the following grid, where 456 // SC1 has columnWeight=1 and SR1 has rowWeight=1. 457 // (SC=Space for Column, SR=Space for Row) 458 // 459 // +------+-------------------------------+------+ 460 // | | | | 461 // | SCR0 | SC1 | SC2 | 462 // | | | | 463 // +------+-------------------------------+------+ 464 // | | | | 465 // | | | | 466 // | | | | 467 // | SR1 | | | 468 // | | | | 469 // | | | | 470 // | | | | 471 // +------+-------------------------------+------+ 472 // | | | | 473 // | SR2 | | | 474 // | | | | 475 // +------+-------------------------------+------+ 476 // 477 // Note that when we split columns and rows here, if splitting the expanding 478 // row or column then the row or column weight should be moved to the right or 479 // bottom half! 480 481 482 int columnX = mGrid.getColumnX(column); 483 int rowY = mGrid.getRowY(row); 484 485 targetNode.setAttribute(ANDROID_URI, ATTR_COLUMN_COUNT, VALUE_1); 486 //targetNode.setAttribute(ANDROID_URI, ATTR_COLUMN_COUNT, "3"); 487 //INode scr0 = addSpacer(targetNode, -1, 0, 0, 1, 1); 488 //INode sc1 = addSpacer(targetNode, -1, 0, 1, 0, 0); 489 //INode sc2 = addSpacer(targetNode, -1, 0, 2, 1, 0); 490 //INode sr1 = addSpacer(targetNode, -1, 1, 0, 0, 0); 491 //INode sr2 = addSpacer(targetNode, -1, 2, 0, 0, 1); 492 //sc1.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN_WEIGHT, VALUE_1); 493 //sr1.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW_WEIGHT, VALUE_1); 494 //sc1.setAttribute(ANDROID_URI, ATTR_LAYOUT_GRAVITY, VALUE_FILL_HORIZONTAL); 495 //sr1.setAttribute(ANDROID_URI, ATTR_LAYOUT_GRAVITY, VALUE_FILL_VERTICAL); 496 497 mGrid.loadFromXml(); 498 column = mGrid.getColumn(columnX); 499 row = mGrid.getRow(rowY); 500 } 501 502 int startX, endX; 503 if (mColumnMatch.type == SegmentType.RIGHT) { 504 endX = mColumnMatch.matchedLine - 1; 505 startX = endX - bounds.w; 506 column = mGrid.getColumn(startX); 507 } else { 508 startX = mColumnMatch.matchedLine; // TODO: What happens on type=RIGHT? 509 endX = startX + bounds.w; 510 } 511 int startY, endY; 512 if (mRowMatch.type == SegmentType.BOTTOM) { 513 endY = mRowMatch.matchedLine - 1; 514 startY = endY - bounds.h; 515 row = mGrid.getRow(startY); 516 } else if (mRowMatch.type == SegmentType.BASELINE) { 517 // TODO: The rowSpan should always be 1 for baseline alignments, since 518 // otherwise the alignment won't work! 519 startY = endY = mRowMatch.matchedLine; 520 } else { 521 startY = mRowMatch.matchedLine; 522 endY = startY + bounds.h; 523 } 524 int endColumn = mGrid.getColumn(endX); 525 int endRow = mGrid.getRow(endY); 526 int columnSpan = endColumn - column + 1; 527 int rowSpan = endRow - row + 1; 528 529 // Make sure my math was right: 530 if (mRowMatch.type == SegmentType.BASELINE) { 531 assert rowSpan == 1 : rowSpan; 532 } 533 534 // If the item almost fits into the row (at most N % bigger) then just enlarge 535 // the row; don't add a rowspan since that will defeat baseline alignment etc 536 if (!mRowMatch.createCell && bounds.h <= MAX_CELL_DIFFERENCE * mGrid.getRowHeight( 537 mRowMatch.type == SegmentType.BOTTOM ? endRow : row, 1)) { 538 if (mRowMatch.type == SegmentType.BOTTOM) { 539 row += rowSpan - 1; 540 } 541 rowSpan = 1; 542 } 543 if (!mColumnMatch.createCell && bounds.w <= MAX_CELL_DIFFERENCE * mGrid.getColumnWidth( 544 mColumnMatch.type == SegmentType.RIGHT ? endColumn : column, 1)) { 545 if (mColumnMatch.type == SegmentType.RIGHT) { 546 column += columnSpan - 1; 547 } 548 columnSpan = 1; 549 } 550 551 if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) { 552 column = 0; 553 columnSpan = mGrid.actualColumnCount; 554 } 555 556 // Temporary: Ensure we don't get in trouble with implicit positions 557 mGrid.applyPositionAttributes(); 558 559 // Split cells to make a new column 560 if (mColumnMatch.createCell) { 561 int columnWidthPx = mGrid.getColumnDistance(column, mColumnMatch.matchedLine); 562 //assert columnWidthPx == columnMatch.distance; // TBD? IF so simplify 563 int columnWidthDp = mRule.getRulesEngine().pxToDp(columnWidthPx); 564 565 int maxX = mGrid.getColumnMaxX(column); 566 boolean insertMarginColumn = false; 567 if (mColumnMatch.margin == 0) { 568 columnWidthDp = 0; 569 } else if (mColumnMatch.margin != UNDEFINED) { 570 int distance = abs(mColumnMatch.matchedLine - (maxX + mColumnMatch.margin)); 571 insertMarginColumn = column > 0 && distance < 2; 572 if (insertMarginColumn) { 573 int margin = mColumnMatch.margin; 574 if (ViewMetadataRepository.INSETS_SUPPORTED) { 575 IViewMetadata metadata = mRule.getRulesEngine().getMetadata(fqcn); 576 if (metadata != null) { 577 Margins insets = metadata.getInsets(); 578 if (insets != null) { 579 // TODO: 580 // Consider left or right side attachment 581 // TODO: Also consider inset of element on cell to the left 582 margin -= insets.left; 583 } 584 } 585 } 586 587 columnWidthDp = mRule.getRulesEngine().pxToDp(margin); 588 } 589 } 590 591 column++; 592 mGrid.splitColumn(column, insertMarginColumn, columnWidthDp, mColumnMatch.matchedLine); 593 if (insertMarginColumn) { 594 column++; 595 } 596 // TODO: This grid refresh is a little risky because we may have added a new 597 // child (spacer) which has no bounds yet! 598 mGrid.loadFromXml(); 599 } 600 601 // Split cells to make a new row 602 if (mRowMatch.createCell) { 603 int rowHeightPx = mGrid.getRowDistance(row, mRowMatch.matchedLine); 604 //assert rowHeightPx == rowMatch.distance; // TBD? If so simplify 605 int rowHeightDp = mRule.getRulesEngine().pxToDp(rowHeightPx); 606 607 int maxY = mGrid.getRowMaxY(row); 608 boolean insertMarginRow = false; 609 if (mRowMatch.margin == 0) { 610 rowHeightDp = 0; 611 } else if (mRowMatch.margin != UNDEFINED) { 612 int distance = abs(mRowMatch.matchedLine - (maxY + mRowMatch.margin)); 613 insertMarginRow = row > 0 && distance < 2; 614 if (insertMarginRow) { 615 int margin = mRowMatch.margin; 616 IViewMetadata metadata = mRule.getRulesEngine().getMetadata(element.getFqcn()); 617 if (metadata != null) { 618 Margins insets = metadata.getInsets(); 619 if (insets != null) { 620 // TODO: 621 // Consider left or right side attachment 622 // TODO: Also consider inset of element on cell to the left 623 margin -= insets.top; 624 } 625 } 626 627 rowHeightDp = mRule.getRulesEngine().pxToDp(margin); 628 } 629 } 630 631 row++; 632 mGrid.splitRow(row, insertMarginRow, rowHeightDp, mRowMatch.matchedLine); 633 if (insertMarginRow) { 634 row++; 635 } 636 mGrid.loadFromXml(); 637 } 638 639 // Figure out where to insert the new child 640 641 int index = mGrid.getInsertIndex(row, column); 642 if (index == -1) { 643 // Couldn't find a later place to insert 644 newChild = targetNode.appendChild(fqcn); 645 } else { 646 GridModel.ViewData next = mGrid.getView(index); 647 648 newChild = targetNode.insertChildAt(fqcn, index); 649 650 // Must also apply positions to the following child to ensure 651 // that the new child doesn't affect the implicit numbering! 652 // TODO: We can later check whether the implied number is equal to 653 // what it already is such that we don't need this 654 next.applyPositionAttributes(); 655 } 656 657 // Set the cell position of the new widget 658 if (mColumnMatch.type == SegmentType.RIGHT) { 659 newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_GRAVITY, VALUE_RIGHT); 660 } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) { 661 newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_GRAVITY, VALUE_CENTER_HORIZONTAL); 662 } 663 newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN, Integer.toString(column)); 664 if (mRowMatch.type == SegmentType.BOTTOM) { 665 String value = VALUE_BOTTOM; 666 if (mColumnMatch.type == SegmentType.RIGHT) { 667 value = value + '|' + VALUE_RIGHT; 668 } 669 newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_GRAVITY, value); 670 } 671 newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, Integer.toString(row)); 672 673 // Apply spans to ensure that the widget can fit without pushing columns 674 if (columnSpan > 1) { 675 newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN_SPAN, 676 Integer.toString(columnSpan)); 677 } 678 if (rowSpan > 1) { 679 newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW_SPAN, Integer.toString(rowSpan)); 680 } 681 682 // Ensure that we don't store columnCount=0 683 if (mGrid.actualColumnCount == 0) { 684 mGrid.layout.setAttribute(ANDROID_URI, ATTR_COLUMN_COUNT, VALUE_1); 685 } 686 687 return newChild; 688 } 689 690 /** 691 * Called when a drop is completed and we're in grid-editing mode. This will insert 692 * the dragged element into the target cell. 693 * 694 * @param targetNode the GridLayout node 695 * @param element the dragged element 696 * @return the newly created node 697 */ 698 public INode handleGridModeDrop(INode targetNode, IDragElement element) { 699 String fqcn = element.getFqcn(); 700 INode newChild = targetNode.appendChild(fqcn); 701 702 if (mColumnMatch.createCell) { 703 mGrid.addColumn(mColumnMatch.cellIndex, 704 newChild, UNDEFINED, false, UNDEFINED, UNDEFINED); 705 } 706 if (mRowMatch.createCell) { 707 mGrid.loadFromXml(); 708 mGrid.addRow(mRowMatch.cellIndex, newChild, UNDEFINED, false, UNDEFINED, UNDEFINED); 709 } 710 711 newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN, 712 Integer.toString(mColumnMatch.cellIndex)); 713 newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, 714 Integer.toString(mRowMatch.cellIndex)); 715 716 return newChild; 717 } 718 719 /** 720 * Returns the best horizontal match 721 * 722 * @return the best horizontal match, or null if there is no match 723 */ 724 public GridMatch getColumnMatch() { 725 return mColumnMatch; 726 } 727 728 /** 729 * Returns the best vertical match 730 * 731 * @return the best vertical match, or null if there is no match 732 */ 733 public GridMatch getRowMatch() { 734 return mRowMatch; 735 } 736 737 /** 738 * Returns the grid used by the drop handler 739 * 740 * @return the grid used by the drop handler, never null 741 */ 742 public GridModel getGrid() { 743 return mGrid; 744 } 745 }