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.SdkConstants.ATTR_COLUMN_COUNT; 19 import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN; 20 import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN; 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_LAYOUT_ROW_SPAN; 24 import static com.android.ide.common.layout.GravityHelper.getGravity; 25 import static com.android.ide.common.layout.GridLayoutRule.GRID_SIZE; 26 import static com.android.ide.common.layout.GridLayoutRule.MARGIN_SIZE; 27 import static com.android.ide.common.layout.GridLayoutRule.MAX_CELL_DIFFERENCE; 28 import static com.android.ide.common.layout.GridLayoutRule.SHORT_GAP_DP; 29 import static com.android.ide.common.layout.grid.GridModel.UNDEFINED; 30 import static java.lang.Math.abs; 31 32 import com.android.ide.common.api.DropFeedback; 33 import com.android.ide.common.api.IDragElement; 34 import com.android.ide.common.api.INode; 35 import com.android.ide.common.api.IViewMetadata; 36 import com.android.ide.common.api.Margins; 37 import com.android.ide.common.api.Point; 38 import com.android.ide.common.api.Rect; 39 import com.android.ide.common.api.SegmentType; 40 import com.android.ide.common.layout.BaseLayoutRule; 41 import com.android.ide.common.layout.GravityHelper; 42 import com.android.ide.common.layout.GridLayoutRule; 43 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository; 44 45 import java.util.ArrayList; 46 import java.util.Collection; 47 import java.util.Collections; 48 import java.util.List; 49 import java.util.Locale; 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 = GridModel.get(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 Rect dragBounds = feedback.dragBounds; 88 int w = dragBounds != null ? dragBounds.w : 0; 89 int h = dragBounds != null ? dragBounds.h : 0; 90 if (!GridLayoutRule.sGridMode) { 91 if (dragBounds != null) { 92 // Sometimes the items are centered under the mouse so 93 // offset by the top left corner distance 94 x1 += dragBounds.x; 95 y1 += dragBounds.y; 96 } 97 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 = Math.min(column + 1, mGrid.actualColumnCount); 190 leftDistance = rightDistance; 191 } 192 if (bottomDistance < radius + SLOP) { 193 row = Math.min(row + 1, mGrid.actualRowCount); 194 topDistance = bottomDistance; 195 } 196 197 boolean createColumn = leftDistance < radius + SLOP; 198 boolean createRow = topDistance < radius + SLOP; 199 if (x1 >= bounds.x2()) { 200 createColumn = true; 201 } 202 if (y1 >= bounds.y2()) { 203 createRow = true; 204 } 205 206 int cellWidth = leftDistance + rightDistance; 207 int cellHeight = topDistance + bottomDistance; 208 SegmentType horizontalType = SegmentType.LEFT; 209 SegmentType verticalType = SegmentType.TOP; 210 int minDistance = 10; // Don't center or right/bottom align in tiny cells 211 if (!createColumn && leftDistance > minDistance 212 && dragBounds != null && dragBounds.w < cellWidth - 10) { 213 if (rightDistance < leftDistance) { 214 horizontalType = SegmentType.RIGHT; 215 } 216 217 int centerDistance = Math.abs(cellWidth / 2 - leftDistance); 218 if (centerDistance < leftDistance / 2 && centerDistance < rightDistance / 2) { 219 horizontalType = SegmentType.CENTER_HORIZONTAL; 220 } 221 } 222 if (!createRow && topDistance > minDistance 223 && dragBounds != null && dragBounds.h < cellHeight - 10) { 224 if (bottomDistance < topDistance) { 225 verticalType = SegmentType.BOTTOM; 226 } 227 int centerDistance = Math.abs(cellHeight / 2 - topDistance); 228 if (centerDistance < topDistance / 2 && centerDistance < bottomDistance / 2) { 229 verticalType = SegmentType.CENTER_VERTICAL; 230 } 231 } 232 233 mColumnMatch = new GridMatch(horizontalType, 0, x1, column, createColumn, 0); 234 mRowMatch = new GridMatch(verticalType, 0, y1, row, createRow, 0); 235 236 StringBuilder description = new StringBuilder(50); 237 String rowString = Integer.toString(mColumnMatch.cellIndex + 1); 238 String columnString = Integer.toString(mRowMatch.cellIndex + 1); 239 if (mRowMatch.createCell && mRowMatch.cellIndex < mGrid.actualRowCount) { 240 description.append(String.format("Shift row %1$d down", mRowMatch.cellIndex + 1)); 241 description.append('\n'); 242 } 243 if (mColumnMatch.createCell && mColumnMatch.cellIndex < mGrid.actualColumnCount) { 244 description.append(String.format("Shift column %1$d right", 245 mColumnMatch.cellIndex + 1)); 246 description.append('\n'); 247 } 248 description.append(String.format("Insert into cell (%1$s,%2$s)", 249 rowString, columnString)); 250 description.append('\n'); 251 description.append(String.format("Align %1$s, %2$s", 252 horizontalType.name().toLowerCase(Locale.US), 253 verticalType.name().toLowerCase(Locale.US))); 254 feedback.tooltip = description.toString(); 255 } 256 } 257 258 /** 259 * Adds a match to align the left edge with some other edge. 260 */ 261 private void addLeftSideMatch(int x1, List<GridMatch> columnMatches, int max) { 262 int column = (mGrid.getViewCount() == 0) ? 0 : mGrid.getClosestColumn(x1); 263 int columnX = mGrid.getColumnX(column); 264 int distance = abs(columnX - x1); 265 if (distance <= max) { 266 columnMatches.add(new GridMatch(SegmentType.LEFT, distance, columnX, column, 267 false, UNDEFINED)); 268 } 269 } 270 271 /** 272 * Adds a match to align the right edge with some other edge. 273 */ 274 private void addRightSideMatch(int x2, List<GridMatch> columnMatches, int max) { 275 // TODO: Only match the right hand side if the drag bounds fit fully within the 276 // cell! Ditto for match below. 277 int columnRight = (mGrid.getViewCount() == 0) ? 0 : mGrid.getClosestColumn(x2); 278 int rightDistance = mGrid.getColumnDistance(columnRight, x2); 279 if (rightDistance < max) { 280 int columnX = mGrid.getColumnX(columnRight); 281 if (columnX > mGrid.layout.getBounds().x) { 282 columnMatches.add(new GridMatch(SegmentType.RIGHT, rightDistance, columnX, 283 columnRight, false, UNDEFINED)); 284 } 285 } 286 } 287 288 /** 289 * Adds a horizontal match with the center axis of the GridLayout 290 */ 291 private void addCenterColumnMatch(Rect bounds, int x1, int y1, int x2, int y2, 292 List<GridMatch> columnMatches, int max) { 293 Collection<INode> intersectsRow = mGrid.getIntersectsRow(y1, y2); 294 if (intersectsRow.size() == 0) { 295 // Offer centering on this row since there isn't anything there 296 int matchedLine = bounds.centerX(); 297 int distance = abs((x1 + x2) / 2 - matchedLine); 298 if (distance <= 2 * max) { 299 boolean createCell = false; // always just put in column 0 300 columnMatches.add(new GridMatch(SegmentType.CENTER_HORIZONTAL, distance, 301 matchedLine, 0 /* column */, createCell, UNDEFINED)); 302 } 303 } 304 } 305 306 /** 307 * Adds a match to align the top edge with some other edge. 308 */ 309 private void addTopMatch(int y1, List<GridMatch> rowMatches, int max, int row, int rowY) { 310 int distance = mGrid.getRowDistance(row, y1); 311 if (distance <= max) { 312 rowMatches.add(new GridMatch(SegmentType.TOP, distance, rowY, row, false, 313 UNDEFINED)); 314 } 315 } 316 317 /** 318 * Adds a match to align the bottom edge with some other edge. 319 */ 320 private void addBottomMatch(int y2, List<GridMatch> rowMatches, int max) { 321 int rowBottom = (mGrid.getViewCount() == 0) ? 0 : mGrid.getClosestRow(y2); 322 int distance = mGrid.getRowDistance(rowBottom, y2); 323 if (distance < max) { 324 int rowY = mGrid.getRowY(rowBottom); 325 if (rowY > mGrid.layout.getBounds().y) { 326 rowMatches.add(new GridMatch(SegmentType.BOTTOM, distance, rowY, 327 rowBottom, false, UNDEFINED)); 328 } 329 } 330 } 331 332 /** 333 * Adds a baseline match, if applicable. 334 */ 335 private void addBaselineMatch(int dragBaseline, int y1, List<GridMatch> rowMatches, int max, 336 int row, int rowY) { 337 int dragBaselineY = y1 + dragBaseline; 338 int rowBaseline = mGrid.getBaseline(row); 339 if (rowBaseline != -1) { 340 int rowBaselineY = rowY + rowBaseline; 341 int distance = abs(dragBaselineY - rowBaselineY); 342 if (distance < max) { 343 rowMatches.add(new GridMatch(SegmentType.BASELINE, distance, rowBaselineY, row, 344 false, UNDEFINED)); 345 } 346 } 347 } 348 349 /** 350 * Computes a horizontal "gap" match - a preferred distance from the nearest edge, 351 * including margin edges 352 */ 353 private void addColumnGapMatch(Rect bounds, int x1, int x2, List<GridMatch> columnMatches, 354 int max) { 355 if (x1 < bounds.x + MARGIN_SIZE + max) { 356 int matchedLine = bounds.x + MARGIN_SIZE; 357 int distance = abs(matchedLine - x1); 358 if (distance <= max) { 359 boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; 360 columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine, 361 0, createCell, MARGIN_SIZE)); 362 } 363 } else if (x2 > bounds.x2() - MARGIN_SIZE - max) { 364 int matchedLine = bounds.x2() - MARGIN_SIZE; 365 int distance = abs(matchedLine - x2); 366 if (distance <= max) { 367 // This does not yet work properly; we need to use columnWeights to achieve this 368 //boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; 369 //columnMatches.add(new GridMatch(SegmentType.RIGHT, distance, matchedLine, 370 // mGrid.actualColumnCount - 1, createCell, MARGIN_SIZE)); 371 } 372 } else { 373 int columnRight = mGrid.getColumn(x1 - SHORT_GAP_DP); 374 int columnX = mGrid.getColumnMaxX(columnRight); 375 int matchedLine = columnX + SHORT_GAP_DP; 376 int distance = abs(matchedLine - x1); 377 if (distance <= max) { 378 boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; 379 columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine, 380 columnRight, createCell, SHORT_GAP_DP)); 381 } 382 383 // Add a column directly adjacent (no gap) 384 columnRight = mGrid.getColumn(x1); 385 columnX = mGrid.getColumnMaxX(columnRight); 386 matchedLine = columnX; 387 distance = abs(matchedLine - x1); 388 389 // Let's say you have this arrangement: 390 // [button1][button2] 391 // This is two columns, where the right hand side edge of column 1 is 392 // flush with the left side edge of column 2, because in fact the width of 393 // button1 is what defines the width of column 1, and that in turn is what 394 // defines the left side position of column 2. 395 // 396 // In this case we don't want to consider inserting a new column at the 397 // right hand side of button1 a better match than matching left on column 2. 398 // Therefore, to ensure that this doesn't happen, we "penalize" right column 399 // matches such that they don't get preferential treatment when the matching 400 // line is on the left side of the column. 401 distance += 2; 402 403 if (distance <= max) { 404 boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; 405 columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine, 406 columnRight, createCell, 0)); 407 } 408 } 409 } 410 411 /** 412 * Computes a vertical "gap" match - a preferred distance from the nearest edge, 413 * including margin edges 414 */ 415 private void addRowGapMatch(Rect bounds, int y1, int y2, List<GridMatch> rowMatches, int max) { 416 if (y1 < bounds.y + MARGIN_SIZE + max) { 417 int matchedLine = bounds.y + MARGIN_SIZE; 418 int distance = abs(matchedLine - y1); 419 if (distance <= max) { 420 boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; 421 rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine, 422 0, createCell, MARGIN_SIZE)); 423 } 424 } else if (y2 > bounds.y2() - MARGIN_SIZE - max) { 425 int matchedLine = bounds.y2() - MARGIN_SIZE; 426 int distance = abs(matchedLine - y2); 427 if (distance <= max) { 428 // This does not yet work properly; we need to use columnWeights to achieve this 429 //boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; 430 //rowMatches.add(new GridMatch(SegmentType.BOTTOM, distance, matchedLine, 431 // mGrid.actualRowCount - 1, createCell, MARGIN_SIZE)); 432 } 433 } else { 434 int rowBottom = mGrid.getRow(y1 - SHORT_GAP_DP); 435 int rowY = mGrid.getRowMaxY(rowBottom); 436 int matchedLine = rowY + SHORT_GAP_DP; 437 int distance = abs(matchedLine - y1); 438 if (distance <= max) { 439 boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; 440 rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine, 441 rowBottom, createCell, SHORT_GAP_DP)); 442 } 443 444 // Add a row directly adjacent (no gap) 445 rowBottom = mGrid.getRow(y1); 446 rowY = mGrid.getRowMaxY(rowBottom); 447 matchedLine = rowY; 448 distance = abs(matchedLine - y1); 449 distance += 2; // See explanation in addColumnGapMatch 450 if (distance <= max) { 451 boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; 452 rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine, 453 rowBottom, createCell, 0)); 454 } 455 456 } 457 } 458 459 /** 460 * Called when a node is dropped in free-form mode. This will insert the dragged 461 * element into the grid and returns the newly created node. 462 * 463 * @param targetNode the GridLayout node 464 * @param element the dragged element 465 * @return the newly created {@link INode} 466 */ 467 public INode handleFreeFormDrop(INode targetNode, IDragElement element) { 468 assert mRowMatch != null; 469 assert mColumnMatch != null; 470 471 String fqcn = element.getFqcn(); 472 473 INode newChild = null; 474 475 Rect bounds = element.getBounds(); 476 int row = mRowMatch.cellIndex; 477 int column = mColumnMatch.cellIndex; 478 479 if (targetNode.getChildren().length == 0) { 480 // 481 // Set up the initial structure: 482 // 483 // 484 // Fixed Fixed 485 // Size Size 486 // Column Expanding Column Column 487 // +-----+-------------------------------+-----+ 488 // | | | | 489 // | 0,0 | 0,1 | 0,2 | Fixed Size Row 490 // | | | | 491 // +-----+-------------------------------+-----+ 492 // | | | | 493 // | | | | 494 // | | | | 495 // | 1,0 | 1,1 | 1,2 | Expanding Row 496 // | | | | 497 // | | | | 498 // | | | | 499 // +-----+-------------------------------+-----+ 500 // | | | | 501 // | 2,0 | 2,1 | 2,2 | Fixed Size Row 502 // | | | | 503 // +-----+-------------------------------+-----+ 504 // 505 // This is implemented in GridLayout by the following grid, where 506 // SC1 has columnWeight=1 and SR1 has rowWeight=1. 507 // (SC=Space for Column, SR=Space for Row) 508 // 509 // +------+-------------------------------+------+ 510 // | | | | 511 // | SCR0 | SC1 | SC2 | 512 // | | | | 513 // +------+-------------------------------+------+ 514 // | | | | 515 // | | | | 516 // | | | | 517 // | SR1 | | | 518 // | | | | 519 // | | | | 520 // | | | | 521 // +------+-------------------------------+------+ 522 // | | | | 523 // | SR2 | | | 524 // | | | | 525 // +------+-------------------------------+------+ 526 // 527 // Note that when we split columns and rows here, if splitting the expanding 528 // row or column then the row or column weight should be moved to the right or 529 // bottom half! 530 531 532 //int columnX = mGrid.getColumnX(column); 533 //int rowY = mGrid.getRowY(row); 534 535 mGrid.setGridAttribute(targetNode, ATTR_COLUMN_COUNT, 2); 536 //mGrid.setGridAttribute(targetNode, ATTR_COLUMN_COUNT, 3); 537 //INode scr0 = addSpacer(targetNode, -1, 0, 0, 1, 1); 538 //INode sc1 = addSpacer(targetNode, -1, 0, 1, 0, 0); 539 //INode sc2 = addSpacer(targetNode, -1, 0, 2, 1, 0); 540 //INode sr1 = addSpacer(targetNode, -1, 1, 0, 0, 0); 541 //INode sr2 = addSpacer(targetNode, -1, 2, 0, 0, 1); 542 //mGrid.setGridAttribute(sc1, ATTR_LAYOUT_GRAVITY, VALUE_FILL_HORIZONTAL); 543 //mGrid.setGridAttribute(sr1, ATTR_LAYOUT_GRAVITY, VALUE_FILL_VERTICAL); 544 // 545 //mGrid.loadFromXml(); 546 //column = mGrid.getColumn(columnX); 547 //row = mGrid.getRow(rowY); 548 } 549 550 int startX, endX; 551 if (mColumnMatch.type == SegmentType.RIGHT) { 552 endX = mColumnMatch.matchedLine - 1; 553 startX = endX - bounds.w; 554 column = mGrid.getColumn(startX); 555 } else { 556 startX = mColumnMatch.matchedLine; // TODO: What happens on type=RIGHT? 557 endX = startX + bounds.w; 558 } 559 int startY, endY; 560 if (mRowMatch.type == SegmentType.BOTTOM) { 561 endY = mRowMatch.matchedLine - 1; 562 startY = endY - bounds.h; 563 row = mGrid.getRow(startY); 564 } else if (mRowMatch.type == SegmentType.BASELINE) { 565 // TODO: The rowSpan should always be 1 for baseline alignments, since 566 // otherwise the alignment won't work! 567 startY = endY = mRowMatch.matchedLine; 568 } else { 569 startY = mRowMatch.matchedLine; 570 endY = startY + bounds.h; 571 } 572 int endColumn = mGrid.getColumn(endX); 573 int endRow = mGrid.getRow(endY); 574 int columnSpan = endColumn - column + 1; 575 int rowSpan = endRow - row + 1; 576 577 // Make sure my math was right: 578 assert mRowMatch.type != SegmentType.BASELINE || rowSpan == 1 : rowSpan; 579 580 // If the item almost fits into the row (at most N % bigger) then just enlarge 581 // the row; don't add a rowspan since that will defeat baseline alignment etc 582 if (!mRowMatch.createCell && bounds.h <= MAX_CELL_DIFFERENCE * mGrid.getRowHeight( 583 mRowMatch.type == SegmentType.BOTTOM ? endRow : row, 1)) { 584 if (mRowMatch.type == SegmentType.BOTTOM) { 585 row += rowSpan - 1; 586 } 587 rowSpan = 1; 588 } 589 if (!mColumnMatch.createCell && bounds.w <= MAX_CELL_DIFFERENCE * mGrid.getColumnWidth( 590 mColumnMatch.type == SegmentType.RIGHT ? endColumn : column, 1)) { 591 if (mColumnMatch.type == SegmentType.RIGHT) { 592 column += columnSpan - 1; 593 } 594 columnSpan = 1; 595 } 596 597 if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) { 598 column = 0; 599 columnSpan = mGrid.actualColumnCount; 600 } 601 602 // Temporary: Ensure we don't get in trouble with implicit positions 603 mGrid.applyPositionAttributes(); 604 605 // Split cells to make a new column 606 if (mColumnMatch.createCell) { 607 int columnWidthPx = mGrid.getColumnDistance(column, mColumnMatch.matchedLine); 608 //assert columnWidthPx == columnMatch.distance; // TBD? IF so simplify 609 int columnWidthDp = mRule.getRulesEngine().pxToDp(columnWidthPx); 610 611 int maxX = mGrid.getColumnMaxX(column); 612 boolean insertMarginColumn = false; 613 if (mColumnMatch.margin == 0) { 614 columnWidthDp = 0; 615 } else if (mColumnMatch.margin != UNDEFINED) { 616 int distance = abs(mColumnMatch.matchedLine - (maxX + mColumnMatch.margin)); 617 insertMarginColumn = column > 0 && distance < 2; 618 if (insertMarginColumn) { 619 int margin = mColumnMatch.margin; 620 if (ViewMetadataRepository.INSETS_SUPPORTED) { 621 IViewMetadata metadata = mRule.getRulesEngine().getMetadata(fqcn); 622 if (metadata != null) { 623 Margins insets = metadata.getInsets(); 624 if (insets != null) { 625 // TODO: 626 // Consider left or right side attachment 627 // TODO: Also consider inset of element on cell to the left 628 margin -= insets.left; 629 } 630 } 631 } 632 633 columnWidthDp = mRule.getRulesEngine().pxToDp(margin); 634 } 635 } 636 637 column++; 638 mGrid.splitColumn(column, insertMarginColumn, columnWidthDp, mColumnMatch.matchedLine); 639 if (insertMarginColumn) { 640 column++; 641 } 642 } 643 644 // Split cells to make a new row 645 if (mRowMatch.createCell) { 646 int rowHeightPx = mGrid.getRowDistance(row, mRowMatch.matchedLine); 647 //assert rowHeightPx == rowMatch.distance; // TBD? If so simplify 648 int rowHeightDp = mRule.getRulesEngine().pxToDp(rowHeightPx); 649 650 int maxY = mGrid.getRowMaxY(row); 651 boolean insertMarginRow = false; 652 if (mRowMatch.margin == 0) { 653 rowHeightDp = 0; 654 } else if (mRowMatch.margin != UNDEFINED) { 655 int distance = abs(mRowMatch.matchedLine - (maxY + mRowMatch.margin)); 656 insertMarginRow = row > 0 && distance < 2; 657 if (insertMarginRow) { 658 int margin = mRowMatch.margin; 659 IViewMetadata metadata = mRule.getRulesEngine().getMetadata(element.getFqcn()); 660 if (metadata != null) { 661 Margins insets = metadata.getInsets(); 662 if (insets != null) { 663 // TODO: 664 // Consider left or right side attachment 665 // TODO: Also consider inset of element on cell to the left 666 margin -= insets.top; 667 } 668 } 669 670 rowHeightDp = mRule.getRulesEngine().pxToDp(margin); 671 } 672 } 673 674 row++; 675 mGrid.splitRow(row, insertMarginRow, rowHeightDp, mRowMatch.matchedLine); 676 if (insertMarginRow) { 677 row++; 678 } 679 } 680 681 // Figure out where to insert the new child 682 683 int index = mGrid.getInsertIndex(row, column); 684 if (index == -1) { 685 // Couldn't find a later place to insert 686 newChild = targetNode.appendChild(fqcn); 687 } else { 688 GridModel.ViewData next = mGrid.getView(index); 689 690 newChild = targetNode.insertChildAt(fqcn, index); 691 692 // Must also apply positions to the following child to ensure 693 // that the new child doesn't affect the implicit numbering! 694 // TODO: We can later check whether the implied number is equal to 695 // what it already is such that we don't need this 696 next.applyPositionAttributes(); 697 } 698 699 // Set the cell position (gravity) of the new widget 700 int gravity = 0; 701 if (mColumnMatch.type == SegmentType.RIGHT) { 702 gravity |= GravityHelper.GRAVITY_RIGHT; 703 } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) { 704 gravity |= GravityHelper.GRAVITY_CENTER_HORIZ; 705 } 706 mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, column); 707 if (mRowMatch.type == SegmentType.BASELINE) { 708 // There *is* no baseline gravity constant, instead, leave the 709 // vertical gravity unspecified and GridLayout will treat it as 710 // baseline alignment 711 //gravity |= GravityHelper.GRAVITY_BASELINE; 712 } else if (mRowMatch.type == SegmentType.BOTTOM) { 713 gravity |= GravityHelper.GRAVITY_BOTTOM; 714 } else if (mRowMatch.type == SegmentType.CENTER_VERTICAL) { 715 gravity |= GravityHelper.GRAVITY_CENTER_VERT; 716 } 717 // Ensure that we have at least one horizontal and vertical constraint, otherwise 718 // the new item will be fixed. As an example, if we have a single button in the 719 // table which we inserted *without* a gravity, and we then insert a button 720 // above it with a vertical gravity, then only the top column would be considered 721 // stretchable, and it will fill all available vertical space and the previous 722 // button will jump to the bottom. 723 if (!GravityHelper.isConstrainedHorizontally(gravity)) { 724 gravity |= GravityHelper.GRAVITY_LEFT; 725 } 726 /* This causes problems: Try placing two buttons vertically from the top of the layout. 727 We need to solve the free column/free row problem first. 728 if (!GravityHelper.isConstrainedVertically(gravity) 729 // There is no baseline constant, so we have to leave it unconstrained instead 730 && mRowMatch.type != SegmentType.BASELINE 731 // You also can't baseline align one element with another that has vertical 732 // alignment top or bottom, so when we first "freely" place views (e.g. 733 // at a particular y location), also place it freely (no constraint). 734 && !mRowMatch.createCell) { 735 gravity |= GravityHelper.GRAVITY_TOP; 736 } 737 */ 738 mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, getGravity(gravity)); 739 740 mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, row); 741 742 // Apply spans to ensure that the widget can fit without pushing columns 743 if (columnSpan > 1) { 744 mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN_SPAN, columnSpan); 745 } 746 if (rowSpan > 1) { 747 mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW_SPAN, rowSpan); 748 } 749 750 // Ensure that we don't store columnCount=0 751 if (mGrid.actualColumnCount == 0) { 752 mGrid.setGridAttribute(mGrid.layout, ATTR_COLUMN_COUNT, Math.max(1, column + 1)); 753 } 754 755 return newChild; 756 } 757 758 /** 759 * Called when a drop is completed and we're in grid-editing mode. This will insert 760 * the dragged element into the target cell. 761 * 762 * @param targetNode the GridLayout node 763 * @param element the dragged element 764 * @return the newly created node 765 */ 766 public INode handleGridModeDrop(INode targetNode, IDragElement element) { 767 String fqcn = element.getFqcn(); 768 INode newChild = targetNode.appendChild(fqcn); 769 770 int column = mColumnMatch.cellIndex; 771 if (mColumnMatch.createCell) { 772 mGrid.addColumn(column, 773 newChild, UNDEFINED, false, UNDEFINED, UNDEFINED); 774 } 775 int row = mRowMatch.cellIndex; 776 if (mRowMatch.createCell) { 777 mGrid.addRow(row, newChild, UNDEFINED, false, UNDEFINED, UNDEFINED); 778 } 779 780 mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, column); 781 mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, row); 782 783 int gravity = 0; 784 if (mColumnMatch.type == SegmentType.RIGHT) { 785 gravity |= GravityHelper.GRAVITY_RIGHT; 786 } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) { 787 gravity |= GravityHelper.GRAVITY_CENTER_HORIZ; 788 } 789 if (mRowMatch.type == SegmentType.BASELINE) { 790 // There *is* no baseline gravity constant, instead, leave the 791 // vertical gravity unspecified and GridLayout will treat it as 792 // baseline alignment 793 //gravity |= GravityHelper.GRAVITY_BASELINE; 794 } else if (mRowMatch.type == SegmentType.BOTTOM) { 795 gravity |= GravityHelper.GRAVITY_BOTTOM; 796 } else if (mRowMatch.type == SegmentType.CENTER_VERTICAL) { 797 gravity |= GravityHelper.GRAVITY_CENTER_VERT; 798 } 799 if (!GravityHelper.isConstrainedHorizontally(gravity)) { 800 gravity |= GravityHelper.GRAVITY_LEFT; 801 } 802 if (!GravityHelper.isConstrainedVertically(gravity)) { 803 gravity |= GravityHelper.GRAVITY_TOP; 804 } 805 mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, getGravity(gravity)); 806 807 if (mGrid.declaredColumnCount == UNDEFINED || mGrid.declaredColumnCount < column + 1) { 808 mGrid.setGridAttribute(mGrid.layout, ATTR_COLUMN_COUNT, column + 1); 809 } 810 811 return newChild; 812 } 813 814 /** 815 * Returns the best horizontal match 816 * 817 * @return the best horizontal match, or null if there is no match 818 */ 819 public GridMatch getColumnMatch() { 820 return mColumnMatch; 821 } 822 823 /** 824 * Returns the best vertical match 825 * 826 * @return the best vertical match, or null if there is no match 827 */ 828 public GridMatch getRowMatch() { 829 return mRowMatch; 830 } 831 832 /** 833 * Returns the grid used by the drop handler 834 * 835 * @return the grid used by the drop handler, never null 836 */ 837 public GridModel getGrid() { 838 return mGrid; 839 } 840 } 841