| /* |
| * 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; |
| } |
| } |