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