diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java | 840 |
1 files changed, 840 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java new file mode 100644 index 000000000..8bdb56bfe --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java @@ -0,0 +1,840 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.common.layout.grid; + +import static com.android.SdkConstants.ATTR_COLUMN_COUNT; +import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN; +import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN; +import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY; +import static com.android.SdkConstants.ATTR_LAYOUT_ROW; +import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN; +import static com.android.ide.common.layout.GravityHelper.getGravity; +import static com.android.ide.common.layout.GridLayoutRule.GRID_SIZE; +import static com.android.ide.common.layout.GridLayoutRule.MARGIN_SIZE; +import static com.android.ide.common.layout.GridLayoutRule.MAX_CELL_DIFFERENCE; +import static com.android.ide.common.layout.GridLayoutRule.SHORT_GAP_DP; +import static com.android.ide.common.layout.grid.GridModel.UNDEFINED; +import static java.lang.Math.abs; + +import com.android.ide.common.api.DropFeedback; +import com.android.ide.common.api.IDragElement; +import com.android.ide.common.api.INode; +import com.android.ide.common.api.IViewMetadata; +import com.android.ide.common.api.Margins; +import com.android.ide.common.api.Point; +import com.android.ide.common.api.Rect; +import com.android.ide.common.api.SegmentType; +import com.android.ide.common.layout.BaseLayoutRule; +import com.android.ide.common.layout.GravityHelper; +import com.android.ide.common.layout.GridLayoutRule; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * The {@link GridDropHandler} handles drag and drop operations into and within a + * GridLayout, computing guidelines, handling drops to edit the grid model, and so on. + */ +public class GridDropHandler { + private final GridModel mGrid; + private final GridLayoutRule mRule; + private GridMatch mColumnMatch; + private GridMatch mRowMatch; + + /** + * Creates a new {@link GridDropHandler} for + * @param gridLayoutRule the corresponding {@link GridLayoutRule} + * @param layout the GridLayout node + * @param view the view instance of the grid layout receiving the drop + */ + public GridDropHandler(GridLayoutRule gridLayoutRule, INode layout, Object view) { + mRule = gridLayoutRule; + mGrid = GridModel.get(mRule.getRulesEngine(), layout, view); + } + + /** + * Computes the best horizontal and vertical matches for a drag to the given position. + * + * @param feedback a {@link DropFeedback} object containing drag state like the drag + * bounds and the drag baseline + * @param p the mouse position + */ + public void computeMatches(DropFeedback feedback, Point p) { + mRowMatch = mColumnMatch = null; + feedback.tooltip = null; + + Rect bounds = mGrid.layout.getBounds(); + int x1 = p.x; + int y1 = p.y; + + Rect dragBounds = feedback.dragBounds; + int w = dragBounds != null ? dragBounds.w : 0; + int h = dragBounds != null ? dragBounds.h : 0; + if (!GridLayoutRule.sGridMode) { + if (dragBounds != null) { + // Sometimes the items are centered under the mouse so + // offset by the top left corner distance + x1 += dragBounds.x; + y1 += dragBounds.y; + } + + int x2 = x1 + w; + int y2 = y1 + h; + + if (x2 < bounds.x || y2 < bounds.y || x1 > bounds.x2() || y1 > bounds.y2()) { + return; + } + + List<GridMatch> columnMatches = new ArrayList<GridMatch>(); + List<GridMatch> rowMatches = new ArrayList<GridMatch>(); + int max = BaseLayoutRule.getMaxMatchDistance(); + + // Column matches: + addLeftSideMatch(x1, columnMatches, max); + addRightSideMatch(x2, columnMatches, max); + addCenterColumnMatch(bounds, x1, y1, x2, y2, columnMatches, max); + + // Row matches: + int row = (mGrid.getViewCount() == 0) ? 0 : mGrid.getClosestRow(y1); + int rowY = mGrid.getRowY(row); + addTopMatch(y1, rowMatches, max, row, rowY); + addBaselineMatch(feedback.dragBaseline, y1, rowMatches, max, row, rowY); + addBottomMatch(y2, rowMatches, max); + + // Look for gap-matches: Predefined spacing between widgets. + // TODO: Make this use metadata for predefined spacing between + // pairs of types of components. For example, buttons have certain + // inserts in their 9-patch files (depending on the theme) that should + // be considered and subtracted from the overall proposed distance! + addColumnGapMatch(bounds, x1, x2, columnMatches, max); + addRowGapMatch(bounds, y1, y2, rowMatches, max); + + // Fallback: Split existing cell. Also do snap-to-grid. + if (GridLayoutRule.sSnapToGrid) { + x1 = ((x1 - MARGIN_SIZE - bounds.x) / GRID_SIZE) * GRID_SIZE + + MARGIN_SIZE + bounds.x; + y1 = ((y1 - MARGIN_SIZE - bounds.y) / GRID_SIZE) * GRID_SIZE + + MARGIN_SIZE + bounds.y; + x2 = x1 + w; + y2 = y1 + h; + } + + + if (columnMatches.size() == 0 && x1 >= bounds.x) { + // Split the current cell since we have no matches + // TODO: Decide whether it should be gravity left or right... + columnMatches.add(new GridMatch(SegmentType.LEFT, 0, x1, mGrid.getColumn(x1), + true /* createCell */, UNDEFINED)); + } + if (rowMatches.size() == 0 && y1 >= bounds.y) { + rowMatches.add(new GridMatch(SegmentType.TOP, 0, y1, mGrid.getRow(y1), + true /* createCell */, UNDEFINED)); + } + + // Pick best matches + Collections.sort(rowMatches); + Collections.sort(columnMatches); + + mColumnMatch = null; + mRowMatch = null; + String columnDescription = null; + String rowDescription = null; + if (columnMatches.size() > 0) { + mColumnMatch = columnMatches.get(0); + columnDescription = mColumnMatch.getDisplayName(mGrid.layout); + } + if (rowMatches.size() > 0) { + mRowMatch = rowMatches.get(0); + rowDescription = mRowMatch.getDisplayName(mGrid.layout); + } + + if (columnDescription != null && rowDescription != null) { + feedback.tooltip = columnDescription + '\n' + rowDescription; + } + + feedback.invalidTarget = mColumnMatch == null || mRowMatch == null; + } else { + // Find which cell we're inside. + + // TODO: Find out where within the cell we are, and offer to tweak the gravity + // based on the position. + int column = mGrid.getColumn(x1); + int row = mGrid.getRow(y1); + + int leftDistance = mGrid.getColumnDistance(column, x1); + int rightDistance = mGrid.getColumnDistance(column + 1, x1); + int topDistance = mGrid.getRowDistance(row, y1); + int bottomDistance = mGrid.getRowDistance(row + 1, y1); + + int SLOP = 2; + int radius = mRule.getNewCellSize(); + if (rightDistance < radius + SLOP) { + column = Math.min(column + 1, mGrid.actualColumnCount); + leftDistance = rightDistance; + } + if (bottomDistance < radius + SLOP) { + row = Math.min(row + 1, mGrid.actualRowCount); + topDistance = bottomDistance; + } + + boolean createColumn = leftDistance < radius + SLOP; + boolean createRow = topDistance < radius + SLOP; + if (x1 >= bounds.x2()) { + createColumn = true; + } + if (y1 >= bounds.y2()) { + createRow = true; + } + + int cellWidth = leftDistance + rightDistance; + int cellHeight = topDistance + bottomDistance; + SegmentType horizontalType = SegmentType.LEFT; + SegmentType verticalType = SegmentType.TOP; + int minDistance = 10; // Don't center or right/bottom align in tiny cells + if (!createColumn && leftDistance > minDistance + && dragBounds != null && dragBounds.w < cellWidth - 10) { + if (rightDistance < leftDistance) { + horizontalType = SegmentType.RIGHT; + } + + int centerDistance = Math.abs(cellWidth / 2 - leftDistance); + if (centerDistance < leftDistance / 2 && centerDistance < rightDistance / 2) { + horizontalType = SegmentType.CENTER_HORIZONTAL; + } + } + if (!createRow && topDistance > minDistance + && dragBounds != null && dragBounds.h < cellHeight - 10) { + if (bottomDistance < topDistance) { + verticalType = SegmentType.BOTTOM; + } + int centerDistance = Math.abs(cellHeight / 2 - topDistance); + if (centerDistance < topDistance / 2 && centerDistance < bottomDistance / 2) { + verticalType = SegmentType.CENTER_VERTICAL; + } + } + + mColumnMatch = new GridMatch(horizontalType, 0, x1, column, createColumn, 0); + mRowMatch = new GridMatch(verticalType, 0, y1, row, createRow, 0); + + StringBuilder description = new StringBuilder(50); + String rowString = Integer.toString(mColumnMatch.cellIndex + 1); + String columnString = Integer.toString(mRowMatch.cellIndex + 1); + if (mRowMatch.createCell && mRowMatch.cellIndex < mGrid.actualRowCount) { + description.append(String.format("Shift row %1$d down", mRowMatch.cellIndex + 1)); + description.append('\n'); + } + if (mColumnMatch.createCell && mColumnMatch.cellIndex < mGrid.actualColumnCount) { + description.append(String.format("Shift column %1$d right", + mColumnMatch.cellIndex + 1)); + description.append('\n'); + } + description.append(String.format("Insert into cell (%1$s,%2$s)", + rowString, columnString)); + description.append('\n'); + description.append(String.format("Align %1$s, %2$s", + horizontalType.name().toLowerCase(Locale.US), + verticalType.name().toLowerCase(Locale.US))); + feedback.tooltip = description.toString(); + } + } + + /** + * Adds a match to align the left edge with some other edge. + */ + private void addLeftSideMatch(int x1, List<GridMatch> columnMatches, int max) { + int column = (mGrid.getViewCount() == 0) ? 0 : mGrid.getClosestColumn(x1); + int columnX = mGrid.getColumnX(column); + int distance = abs(columnX - x1); + if (distance <= max) { + columnMatches.add(new GridMatch(SegmentType.LEFT, distance, columnX, column, + false, UNDEFINED)); + } + } + + /** + * Adds a match to align the right edge with some other edge. + */ + private void addRightSideMatch(int x2, List<GridMatch> columnMatches, int max) { + // TODO: Only match the right hand side if the drag bounds fit fully within the + // cell! Ditto for match below. + int columnRight = (mGrid.getViewCount() == 0) ? 0 : mGrid.getClosestColumn(x2); + int rightDistance = mGrid.getColumnDistance(columnRight, x2); + if (rightDistance < max) { + int columnX = mGrid.getColumnX(columnRight); + if (columnX > mGrid.layout.getBounds().x) { + columnMatches.add(new GridMatch(SegmentType.RIGHT, rightDistance, columnX, + columnRight, false, UNDEFINED)); + } + } + } + + /** + * Adds a horizontal match with the center axis of the GridLayout + */ + private void addCenterColumnMatch(Rect bounds, int x1, int y1, int x2, int y2, + List<GridMatch> columnMatches, int max) { + Collection<INode> intersectsRow = mGrid.getIntersectsRow(y1, y2); + if (intersectsRow.size() == 0) { + // Offer centering on this row since there isn't anything there + int matchedLine = bounds.centerX(); + int distance = abs((x1 + x2) / 2 - matchedLine); + if (distance <= 2 * max) { + boolean createCell = false; // always just put in column 0 + columnMatches.add(new GridMatch(SegmentType.CENTER_HORIZONTAL, distance, + matchedLine, 0 /* column */, createCell, UNDEFINED)); + } + } + } + + /** + * Adds a match to align the top edge with some other edge. + */ + private void addTopMatch(int y1, List<GridMatch> rowMatches, int max, int row, int rowY) { + int distance = mGrid.getRowDistance(row, y1); + if (distance <= max) { + rowMatches.add(new GridMatch(SegmentType.TOP, distance, rowY, row, false, + UNDEFINED)); + } + } + + /** + * Adds a match to align the bottom edge with some other edge. + */ + private void addBottomMatch(int y2, List<GridMatch> rowMatches, int max) { + int rowBottom = (mGrid.getViewCount() == 0) ? 0 : mGrid.getClosestRow(y2); + int distance = mGrid.getRowDistance(rowBottom, y2); + if (distance < max) { + int rowY = mGrid.getRowY(rowBottom); + if (rowY > mGrid.layout.getBounds().y) { + rowMatches.add(new GridMatch(SegmentType.BOTTOM, distance, rowY, + rowBottom, false, UNDEFINED)); + } + } + } + + /** + * Adds a baseline match, if applicable. + */ + private void addBaselineMatch(int dragBaseline, int y1, List<GridMatch> rowMatches, int max, + int row, int rowY) { + int dragBaselineY = y1 + dragBaseline; + int rowBaseline = mGrid.getBaseline(row); + if (rowBaseline != -1) { + int rowBaselineY = rowY + rowBaseline; + int distance = abs(dragBaselineY - rowBaselineY); + if (distance < max) { + rowMatches.add(new GridMatch(SegmentType.BASELINE, distance, rowBaselineY, row, + false, UNDEFINED)); + } + } + } + + /** + * Computes a horizontal "gap" match - a preferred distance from the nearest edge, + * including margin edges + */ + private void addColumnGapMatch(Rect bounds, int x1, int x2, List<GridMatch> columnMatches, + int max) { + if (x1 < bounds.x + MARGIN_SIZE + max) { + int matchedLine = bounds.x + MARGIN_SIZE; + int distance = abs(matchedLine - x1); + if (distance <= max) { + boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; + columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine, + 0, createCell, MARGIN_SIZE)); + } + } else if (x2 > bounds.x2() - MARGIN_SIZE - max) { + int matchedLine = bounds.x2() - MARGIN_SIZE; + int distance = abs(matchedLine - x2); + if (distance <= max) { + // This does not yet work properly; we need to use columnWeights to achieve this + //boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; + //columnMatches.add(new GridMatch(SegmentType.RIGHT, distance, matchedLine, + // mGrid.actualColumnCount - 1, createCell, MARGIN_SIZE)); + } + } else { + int columnRight = mGrid.getColumn(x1 - SHORT_GAP_DP); + int columnX = mGrid.getColumnMaxX(columnRight); + int matchedLine = columnX + SHORT_GAP_DP; + int distance = abs(matchedLine - x1); + if (distance <= max) { + boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; + columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine, + columnRight, createCell, SHORT_GAP_DP)); + } + + // Add a column directly adjacent (no gap) + columnRight = mGrid.getColumn(x1); + columnX = mGrid.getColumnMaxX(columnRight); + matchedLine = columnX; + distance = abs(matchedLine - x1); + + // Let's say you have this arrangement: + // [button1][button2] + // This is two columns, where the right hand side edge of column 1 is + // flush with the left side edge of column 2, because in fact the width of + // button1 is what defines the width of column 1, and that in turn is what + // defines the left side position of column 2. + // + // In this case we don't want to consider inserting a new column at the + // right hand side of button1 a better match than matching left on column 2. + // Therefore, to ensure that this doesn't happen, we "penalize" right column + // matches such that they don't get preferential treatment when the matching + // line is on the left side of the column. + distance += 2; + + if (distance <= max) { + boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; + columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine, + columnRight, createCell, 0)); + } + } + } + + /** + * Computes a vertical "gap" match - a preferred distance from the nearest edge, + * including margin edges + */ + private void addRowGapMatch(Rect bounds, int y1, int y2, List<GridMatch> rowMatches, int max) { + if (y1 < bounds.y + MARGIN_SIZE + max) { + int matchedLine = bounds.y + MARGIN_SIZE; + int distance = abs(matchedLine - y1); + if (distance <= max) { + boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; + rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine, + 0, createCell, MARGIN_SIZE)); + } + } else if (y2 > bounds.y2() - MARGIN_SIZE - max) { + int matchedLine = bounds.y2() - MARGIN_SIZE; + int distance = abs(matchedLine - y2); + if (distance <= max) { + // This does not yet work properly; we need to use columnWeights to achieve this + //boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; + //rowMatches.add(new GridMatch(SegmentType.BOTTOM, distance, matchedLine, + // mGrid.actualRowCount - 1, createCell, MARGIN_SIZE)); + } + } else { + int rowBottom = mGrid.getRow(y1 - SHORT_GAP_DP); + int rowY = mGrid.getRowMaxY(rowBottom); + int matchedLine = rowY + SHORT_GAP_DP; + int distance = abs(matchedLine - y1); + if (distance <= max) { + boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; + rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine, + rowBottom, createCell, SHORT_GAP_DP)); + } + + // Add a row directly adjacent (no gap) + rowBottom = mGrid.getRow(y1); + rowY = mGrid.getRowMaxY(rowBottom); + matchedLine = rowY; + distance = abs(matchedLine - y1); + distance += 2; // See explanation in addColumnGapMatch + if (distance <= max) { + boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; + rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine, + rowBottom, createCell, 0)); + } + + } + } + + /** + * Called when a node is dropped in free-form mode. This will insert the dragged + * element into the grid and returns the newly created node. + * + * @param targetNode the GridLayout node + * @param element the dragged element + * @return the newly created {@link INode} + */ + public INode handleFreeFormDrop(INode targetNode, IDragElement element) { + assert mRowMatch != null; + assert mColumnMatch != null; + + String fqcn = element.getFqcn(); + + INode newChild = null; + + Rect bounds = element.getBounds(); + int row = mRowMatch.cellIndex; + int column = mColumnMatch.cellIndex; + + if (targetNode.getChildren().length == 0) { + // + // Set up the initial structure: + // + // + // Fixed Fixed + // Size Size + // Column Expanding Column Column + // +-----+-------------------------------+-----+ + // | | | | + // | 0,0 | 0,1 | 0,2 | Fixed Size Row + // | | | | + // +-----+-------------------------------+-----+ + // | | | | + // | | | | + // | | | | + // | 1,0 | 1,1 | 1,2 | Expanding Row + // | | | | + // | | | | + // | | | | + // +-----+-------------------------------+-----+ + // | | | | + // | 2,0 | 2,1 | 2,2 | Fixed Size Row + // | | | | + // +-----+-------------------------------+-----+ + // + // This is implemented in GridLayout by the following grid, where + // SC1 has columnWeight=1 and SR1 has rowWeight=1. + // (SC=Space for Column, SR=Space for Row) + // + // +------+-------------------------------+------+ + // | | | | + // | SCR0 | SC1 | SC2 | + // | | | | + // +------+-------------------------------+------+ + // | | | | + // | | | | + // | | | | + // | SR1 | | | + // | | | | + // | | | | + // | | | | + // +------+-------------------------------+------+ + // | | | | + // | SR2 | | | + // | | | | + // +------+-------------------------------+------+ + // + // Note that when we split columns and rows here, if splitting the expanding + // row or column then the row or column weight should be moved to the right or + // bottom half! + + + //int columnX = mGrid.getColumnX(column); + //int rowY = mGrid.getRowY(row); + + mGrid.setGridAttribute(targetNode, ATTR_COLUMN_COUNT, 2); + //mGrid.setGridAttribute(targetNode, ATTR_COLUMN_COUNT, 3); + //INode scr0 = addSpacer(targetNode, -1, 0, 0, 1, 1); + //INode sc1 = addSpacer(targetNode, -1, 0, 1, 0, 0); + //INode sc2 = addSpacer(targetNode, -1, 0, 2, 1, 0); + //INode sr1 = addSpacer(targetNode, -1, 1, 0, 0, 0); + //INode sr2 = addSpacer(targetNode, -1, 2, 0, 0, 1); + //mGrid.setGridAttribute(sc1, ATTR_LAYOUT_GRAVITY, VALUE_FILL_HORIZONTAL); + //mGrid.setGridAttribute(sr1, ATTR_LAYOUT_GRAVITY, VALUE_FILL_VERTICAL); + // + //mGrid.loadFromXml(); + //column = mGrid.getColumn(columnX); + //row = mGrid.getRow(rowY); + } + + int startX, endX; + if (mColumnMatch.type == SegmentType.RIGHT) { + endX = mColumnMatch.matchedLine - 1; + startX = endX - bounds.w; + column = mGrid.getColumn(startX); + } else { + startX = mColumnMatch.matchedLine; // TODO: What happens on type=RIGHT? + endX = startX + bounds.w; + } + int startY, endY; + if (mRowMatch.type == SegmentType.BOTTOM) { + endY = mRowMatch.matchedLine - 1; + startY = endY - bounds.h; + row = mGrid.getRow(startY); + } else if (mRowMatch.type == SegmentType.BASELINE) { + // TODO: The rowSpan should always be 1 for baseline alignments, since + // otherwise the alignment won't work! + startY = endY = mRowMatch.matchedLine; + } else { + startY = mRowMatch.matchedLine; + endY = startY + bounds.h; + } + int endColumn = mGrid.getColumn(endX); + int endRow = mGrid.getRow(endY); + int columnSpan = endColumn - column + 1; + int rowSpan = endRow - row + 1; + + // Make sure my math was right: + assert mRowMatch.type != SegmentType.BASELINE || rowSpan == 1 : rowSpan; + + // If the item almost fits into the row (at most N % bigger) then just enlarge + // the row; don't add a rowspan since that will defeat baseline alignment etc + if (!mRowMatch.createCell && bounds.h <= MAX_CELL_DIFFERENCE * mGrid.getRowHeight( + mRowMatch.type == SegmentType.BOTTOM ? endRow : row, 1)) { + if (mRowMatch.type == SegmentType.BOTTOM) { + row += rowSpan - 1; + } + rowSpan = 1; + } + if (!mColumnMatch.createCell && bounds.w <= MAX_CELL_DIFFERENCE * mGrid.getColumnWidth( + mColumnMatch.type == SegmentType.RIGHT ? endColumn : column, 1)) { + if (mColumnMatch.type == SegmentType.RIGHT) { + column += columnSpan - 1; + } + columnSpan = 1; + } + + if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) { + column = 0; + columnSpan = mGrid.actualColumnCount; + } + + // Temporary: Ensure we don't get in trouble with implicit positions + mGrid.applyPositionAttributes(); + + // Split cells to make a new column + if (mColumnMatch.createCell) { + int columnWidthPx = mGrid.getColumnDistance(column, mColumnMatch.matchedLine); + //assert columnWidthPx == columnMatch.distance; // TBD? IF so simplify + int columnWidthDp = mRule.getRulesEngine().pxToDp(columnWidthPx); + + int maxX = mGrid.getColumnMaxX(column); + boolean insertMarginColumn = false; + if (mColumnMatch.margin == 0) { + columnWidthDp = 0; + } else if (mColumnMatch.margin != UNDEFINED) { + int distance = abs(mColumnMatch.matchedLine - (maxX + mColumnMatch.margin)); + insertMarginColumn = column > 0 && distance < 2; + if (insertMarginColumn) { + int margin = mColumnMatch.margin; + if (ViewMetadataRepository.INSETS_SUPPORTED) { + IViewMetadata metadata = mRule.getRulesEngine().getMetadata(fqcn); + if (metadata != null) { + Margins insets = metadata.getInsets(); + if (insets != null) { + // TODO: + // Consider left or right side attachment + // TODO: Also consider inset of element on cell to the left + margin -= insets.left; + } + } + } + + columnWidthDp = mRule.getRulesEngine().pxToDp(margin); + } + } + + column++; + mGrid.splitColumn(column, insertMarginColumn, columnWidthDp, mColumnMatch.matchedLine); + if (insertMarginColumn) { + column++; + } + } + + // Split cells to make a new row + if (mRowMatch.createCell) { + int rowHeightPx = mGrid.getRowDistance(row, mRowMatch.matchedLine); + //assert rowHeightPx == rowMatch.distance; // TBD? If so simplify + int rowHeightDp = mRule.getRulesEngine().pxToDp(rowHeightPx); + + int maxY = mGrid.getRowMaxY(row); + boolean insertMarginRow = false; + if (mRowMatch.margin == 0) { + rowHeightDp = 0; + } else if (mRowMatch.margin != UNDEFINED) { + int distance = abs(mRowMatch.matchedLine - (maxY + mRowMatch.margin)); + insertMarginRow = row > 0 && distance < 2; + if (insertMarginRow) { + int margin = mRowMatch.margin; + IViewMetadata metadata = mRule.getRulesEngine().getMetadata(element.getFqcn()); + if (metadata != null) { + Margins insets = metadata.getInsets(); + if (insets != null) { + // TODO: + // Consider left or right side attachment + // TODO: Also consider inset of element on cell to the left + margin -= insets.top; + } + } + + rowHeightDp = mRule.getRulesEngine().pxToDp(margin); + } + } + + row++; + mGrid.splitRow(row, insertMarginRow, rowHeightDp, mRowMatch.matchedLine); + if (insertMarginRow) { + row++; + } + } + + // Figure out where to insert the new child + + int index = mGrid.getInsertIndex(row, column); + if (index == -1) { + // Couldn't find a later place to insert + newChild = targetNode.appendChild(fqcn); + } else { + GridModel.ViewData next = mGrid.getView(index); + + newChild = targetNode.insertChildAt(fqcn, index); + + // Must also apply positions to the following child to ensure + // that the new child doesn't affect the implicit numbering! + // TODO: We can later check whether the implied number is equal to + // what it already is such that we don't need this + next.applyPositionAttributes(); + } + + // Set the cell position (gravity) of the new widget + int gravity = 0; + if (mColumnMatch.type == SegmentType.RIGHT) { + gravity |= GravityHelper.GRAVITY_RIGHT; + } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) { + gravity |= GravityHelper.GRAVITY_CENTER_HORIZ; + } + mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, column); + if (mRowMatch.type == SegmentType.BASELINE) { + // There *is* no baseline gravity constant, instead, leave the + // vertical gravity unspecified and GridLayout will treat it as + // baseline alignment + //gravity |= GravityHelper.GRAVITY_BASELINE; + } else if (mRowMatch.type == SegmentType.BOTTOM) { + gravity |= GravityHelper.GRAVITY_BOTTOM; + } else if (mRowMatch.type == SegmentType.CENTER_VERTICAL) { + gravity |= GravityHelper.GRAVITY_CENTER_VERT; + } + // Ensure that we have at least one horizontal and vertical constraint, otherwise + // the new item will be fixed. As an example, if we have a single button in the + // table which we inserted *without* a gravity, and we then insert a button + // above it with a vertical gravity, then only the top column would be considered + // stretchable, and it will fill all available vertical space and the previous + // button will jump to the bottom. + if (!GravityHelper.isConstrainedHorizontally(gravity)) { + gravity |= GravityHelper.GRAVITY_LEFT; + } + /* This causes problems: Try placing two buttons vertically from the top of the layout. + We need to solve the free column/free row problem first. + if (!GravityHelper.isConstrainedVertically(gravity) + // There is no baseline constant, so we have to leave it unconstrained instead + && mRowMatch.type != SegmentType.BASELINE + // You also can't baseline align one element with another that has vertical + // alignment top or bottom, so when we first "freely" place views (e.g. + // at a particular y location), also place it freely (no constraint). + && !mRowMatch.createCell) { + gravity |= GravityHelper.GRAVITY_TOP; + } + */ + mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, getGravity(gravity)); + + mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, row); + + // Apply spans to ensure that the widget can fit without pushing columns + if (columnSpan > 1) { + mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN_SPAN, columnSpan); + } + if (rowSpan > 1) { + mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW_SPAN, rowSpan); + } + + // Ensure that we don't store columnCount=0 + if (mGrid.actualColumnCount == 0) { + mGrid.setGridAttribute(mGrid.layout, ATTR_COLUMN_COUNT, Math.max(1, column + 1)); + } + + return newChild; + } + + /** + * Called when a drop is completed and we're in grid-editing mode. This will insert + * the dragged element into the target cell. + * + * @param targetNode the GridLayout node + * @param element the dragged element + * @return the newly created node + */ + public INode handleGridModeDrop(INode targetNode, IDragElement element) { + String fqcn = element.getFqcn(); + INode newChild = targetNode.appendChild(fqcn); + + int column = mColumnMatch.cellIndex; + if (mColumnMatch.createCell) { + mGrid.addColumn(column, + newChild, UNDEFINED, false, UNDEFINED, UNDEFINED); + } + int row = mRowMatch.cellIndex; + if (mRowMatch.createCell) { + mGrid.addRow(row, newChild, UNDEFINED, false, UNDEFINED, UNDEFINED); + } + + mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, column); + mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, row); + + int gravity = 0; + if (mColumnMatch.type == SegmentType.RIGHT) { + gravity |= GravityHelper.GRAVITY_RIGHT; + } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) { + gravity |= GravityHelper.GRAVITY_CENTER_HORIZ; + } + if (mRowMatch.type == SegmentType.BASELINE) { + // There *is* no baseline gravity constant, instead, leave the + // vertical gravity unspecified and GridLayout will treat it as + // baseline alignment + //gravity |= GravityHelper.GRAVITY_BASELINE; + } else if (mRowMatch.type == SegmentType.BOTTOM) { + gravity |= GravityHelper.GRAVITY_BOTTOM; + } else if (mRowMatch.type == SegmentType.CENTER_VERTICAL) { + gravity |= GravityHelper.GRAVITY_CENTER_VERT; + } + if (!GravityHelper.isConstrainedHorizontally(gravity)) { + gravity |= GravityHelper.GRAVITY_LEFT; + } + if (!GravityHelper.isConstrainedVertically(gravity)) { + gravity |= GravityHelper.GRAVITY_TOP; + } + mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, getGravity(gravity)); + + if (mGrid.declaredColumnCount == UNDEFINED || mGrid.declaredColumnCount < column + 1) { + mGrid.setGridAttribute(mGrid.layout, ATTR_COLUMN_COUNT, column + 1); + } + + return newChild; + } + + /** + * Returns the best horizontal match + * + * @return the best horizontal match, or null if there is no match + */ + public GridMatch getColumnMatch() { + return mColumnMatch; + } + + /** + * Returns the best vertical match + * + * @return the best vertical match, or null if there is no match + */ + public GridMatch getRowMatch() { + return mRowMatch; + } + + /** + * Returns the grid used by the drop handler + * + * @return the grid used by the drop handler, never null + */ + public GridModel getGrid() { + return mGrid; + } +} |