blob: 8bdb56bfe22e6c4c6086d7e621a1527f634d4fd6 [file] [log] [blame]
/*
* 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;
}
}