aboutsummaryrefslogtreecommitdiffstats
path: root/eclipse
diff options
context:
space:
mode:
authorTor Norbye <tnorbye@google.com>2012-06-12 15:23:06 -0700
committerandroid code review <noreply-gerritcodereview@google.com>2012-06-12 15:23:07 -0700
commite618207a9885a7ce67b14b0e25f86186d4943407 (patch)
tree27032b8062b37c719c2d9e5a3851049b8398fcb3 /eclipse
parenta3d5385c0e2138b8e215196bdb3734a44b55d31d (diff)
parentfb6d52d71a7461cb1ac4149f4f71d6a63e7436da (diff)
downloadsdk-e618207a9885a7ce67b14b0e25f86186d4943407.zip
sdk-e618207a9885a7ce67b14b0e25f86186d4943407.tar.gz
sdk-e618207a9885a7ce67b14b0e25f86186d4943407.tar.bz2
Merge "GridLayout support work"
Diffstat (limited to 'eclipse')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GravityHelper.java107
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java35
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridLayoutPainter.java28
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridMatch.java14
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridModel.java755
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java26
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java17
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java14
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/GravityHelperTest.java53
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java22
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java221
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/grid/GridModelTest.java802
13 files changed, 1830 insertions, 311 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GravityHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GravityHelper.java
index aa9a089..6516938 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GravityHelper.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GravityHelper.java
@@ -15,7 +15,6 @@
*/
package com.android.ide.common.layout;
-import static com.android.util.XmlUtils.ANDROID_URI;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_GRAVITY;
import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_BOTTOM;
import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_CENTER;
@@ -27,21 +26,41 @@ import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_V
import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_LEFT;
import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_RIGHT;
import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_TOP;
+import static com.android.util.XmlUtils.ANDROID_URI;
import org.w3c.dom.Element;
/** Helper class for looking up the gravity masks of gravity attributes */
public class GravityHelper {
+ /** Bitmask for a gravity which includes left */
public static final int GRAVITY_LEFT = 1 << 0;
+
+ /** Bitmask for a gravity which includes right */
public static final int GRAVITY_RIGHT = 1 << 1;
+
+ /** Bitmask for a gravity which includes center horizontal */
public static final int GRAVITY_CENTER_HORIZ = 1 << 2;
+
+ /** Bitmask for a gravity which includes fill horizontal */
public static final int GRAVITY_FILL_HORIZ = 1 << 3;
+
+ /** Bitmask for a gravity which includes center vertical */
public static final int GRAVITY_CENTER_VERT = 1 << 4;
+
+ /** Bitmask for a gravity which includes fill vertical */
public static final int GRAVITY_FILL_VERT = 1 << 5;
+
+ /** Bitmask for a gravity which includes top */
public static final int GRAVITY_TOP = 1 << 6;
+
+ /** Bitmask for a gravity which includes bottom */
public static final int GRAVITY_BOTTOM = 1 << 7;
+
+ /** Bitmask for a gravity which includes any horizontal constraint */
public static final int GRAVITY_HORIZ_MASK = GRAVITY_CENTER_HORIZ | GRAVITY_FILL_HORIZ
| GRAVITY_LEFT | GRAVITY_RIGHT;
+
+ /** Bitmask for a gravity which any vertical constraint */
public static final int GRAVITY_VERT_MASK = GRAVITY_CENTER_VERT | GRAVITY_FILL_VERT
| GRAVITY_TOP | GRAVITY_BOTTOM;
@@ -96,4 +115,90 @@ public class GravityHelper {
return gravity;
}
+
+ /**
+ * Returns true if the given gravity bitmask is constrained horizontally
+ *
+ * @param gravity the gravity bitmask
+ * @return true if the given gravity bitmask is constrained horizontally
+ */
+ public static boolean isConstrainedHorizontally(int gravity) {
+ return (gravity & GRAVITY_HORIZ_MASK) != 0;
+ }
+
+ /**
+ * Returns true if the given gravity bitmask is constrained vertically
+ *
+ * @param gravity the gravity bitmask
+ * @return true if the given gravity bitmask is constrained vertically
+ */
+ public static boolean isConstrainedVertically(int gravity) {
+ return (gravity & GRAVITY_VERT_MASK) != 0;
+ }
+
+ /**
+ * Returns true if the given gravity bitmask is left aligned
+ *
+ * @param gravity the gravity bitmask
+ * @return true if the given gravity bitmask is left aligned
+ */
+ public static boolean isLeftAligned(int gravity) {
+ return (gravity & GRAVITY_LEFT) != 0;
+ }
+
+ /**
+ * Returns true if the given gravity bitmask is top aligned
+ *
+ * @param gravity the gravity bitmask
+ * @return true if the given gravity bitmask is aligned
+ */
+ public static boolean isTopAligned(int gravity) {
+ return (gravity & GRAVITY_TOP) != 0;
+ }
+
+ /** Returns a gravity value string from the given gravity bitmask
+ *
+ * @param gravity the gravity bitmask
+ * @return the corresponding gravity string suitable as an XML attribute value
+ */
+ public static String getGravity(int gravity) {
+ if (gravity == 0) {
+ return "";
+ }
+
+ if ((gravity & (GRAVITY_CENTER_HORIZ | GRAVITY_CENTER_VERT)) ==
+ (GRAVITY_CENTER_HORIZ | GRAVITY_CENTER_VERT)) {
+ return GRAVITY_VALUE_CENTER;
+ }
+
+ StringBuilder sb = new StringBuilder(30);
+ int horizontal = gravity & GRAVITY_HORIZ_MASK;
+ int vertical = gravity & GRAVITY_VERT_MASK;
+
+ if ((horizontal & GRAVITY_LEFT) != 0) {
+ sb.append(GRAVITY_VALUE_LEFT);
+ } else if ((horizontal & GRAVITY_RIGHT) != 0) {
+ sb.append(GRAVITY_VALUE_RIGHT);
+ } else if ((horizontal & GRAVITY_CENTER_HORIZ) != 0) {
+ sb.append(GRAVITY_VALUE_CENTER_HORIZONTAL);
+ } else if ((horizontal & GRAVITY_FILL_HORIZ) != 0) {
+ sb.append(GRAVITY_VALUE_FILL_HORIZONTAL);
+ }
+
+ if (sb.length() > 0 && vertical != 0) {
+ sb.append('|');
+ }
+
+ if ((vertical & GRAVITY_TOP) != 0) {
+ sb.append(GRAVITY_VALUE_TOP);
+ } else if ((vertical & GRAVITY_BOTTOM) != 0) {
+ sb.append(GRAVITY_VALUE_BOTTOM);
+ } else if ((vertical & GRAVITY_CENTER_VERT) != 0) {
+ sb.append(GRAVITY_VALUE_CENTER_VERTICAL);
+ } else if ((vertical & GRAVITY_FILL_VERT) != 0) {
+ sb.append(GRAVITY_VALUE_FILL_VERTICAL);
+ }
+
+ return sb.toString();
+ }
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java
index a737251..787a2c2 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java
@@ -205,7 +205,7 @@ public class GridLayoutRule extends BaseLayoutRule {
return;
}
- GridModel grid = new GridModel(mRulesEngine, parentNode, null);
+ GridModel grid = GridModel.get(mRulesEngine, parentNode, null);
if (id.equals(ACTION_ADD_ROW)) {
grid.addRow(children);
} else if (id.equals(ACTION_REMOVE_ROW)) {
@@ -285,6 +285,9 @@ public class GridLayoutRule extends BaseLayoutRule {
@Override
public DropFeedback onDropMove(@NonNull INode targetNode, @NonNull IDragElement[] elements,
@Nullable DropFeedback feedback, @NonNull Point p) {
+ if (feedback == null) {
+ return null;
+ }
feedback.requestPaint = true;
GridDropHandler handler = (GridDropHandler) feedback.userData;
@@ -296,6 +299,10 @@ public class GridLayoutRule extends BaseLayoutRule {
@Override
public void onDropped(final @NonNull INode targetNode, final @NonNull IDragElement[] elements,
@Nullable DropFeedback feedback, @NonNull Point p) {
+ if (feedback == null) {
+ return;
+ }
+
Rect b = targetNode.getBounds();
if (!b.isValid()) {
return;
@@ -334,13 +341,14 @@ public class GridLayoutRule extends BaseLayoutRule {
return;
}
+ if (GridModel.isSpace(node.getFqcn())) {
+ return;
+ }
+
// Attempt to set "fill" properties on newly added views such that for example
// a text field will stretch horizontally.
String fqcn = node.getFqcn();
IViewMetadata metadata = mRulesEngine.getMetadata(fqcn);
- if (metadata == null) {
- return;
- }
FillPreference fill = metadata.getFillPreference();
String gravity = computeDefaultGravity(fill);
if (gravity != null) {
@@ -400,17 +408,8 @@ public class GridLayoutRule extends BaseLayoutRule {
// Attempt to clean up spacer objects for any newly-empty rows or columns
// as the result of this deletion
- GridModel grid = new GridModel(mRulesEngine, parent, null);
- for (INode child : deleted) {
- // We don't care about deletion of spacers
- String fqcn = child.getFqcn();
- if (fqcn.equals(FQCN_SPACE) || fqcn.equals(FQCN_SPACE_V7)) {
- continue;
- }
- grid.markDeleted(child);
- }
-
- grid.cleanup();
+ GridModel grid = GridModel.get(mRulesEngine, parent, null);
+ grid.onDeleted(deleted);
}
@Override
@@ -442,7 +441,7 @@ public class GridLayoutRule extends BaseLayoutRule {
private GridModel getGrid(ResizeState resizeState) {
GridModel grid = (GridModel) resizeState.clientData;
if (grid == null) {
- grid = new GridModel(mRulesEngine, resizeState.layout, resizeState.layoutView);
+ grid = GridModel.get(mRulesEngine, resizeState.layout, resizeState.layoutView);
resizeState.clientData = grid;
}
@@ -543,10 +542,10 @@ public class GridLayoutRule extends BaseLayoutRule {
}
}
GridLayoutPainter.paintStructure(DrawingStyle.GUIDELINE_DASHED,
- parentNode, graphics, new GridModel(mRulesEngine, parentNode, view));
+ parentNode, graphics, GridModel.get(mRulesEngine, parentNode, view));
} else if (sDebugGridLayout) {
GridLayoutPainter.paintStructure(DrawingStyle.GRID,
- parentNode, graphics, new GridModel(mRulesEngine, parentNode, view));
+ parentNode, graphics, GridModel.get(mRulesEngine, parentNode, view));
}
// TBD: Highlight the cells around the selection, and display easy controls
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
index 74c1b59..6d938fb 100644
--- 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
@@ -15,6 +15,7 @@
*/
package com.android.ide.common.layout.grid;
+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;
@@ -25,9 +26,6 @@ import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN_S
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_GRAVITY;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW_SPAN;
-import static com.android.ide.common.layout.LayoutConstants.VALUE_BOTTOM;
-import static com.android.ide.common.layout.LayoutConstants.VALUE_CENTER_HORIZONTAL;
-import static com.android.ide.common.layout.LayoutConstants.VALUE_RIGHT;
import static com.android.ide.common.layout.grid.GridModel.UNDEFINED;
import static java.lang.Math.abs;
@@ -40,6 +38,7 @@ 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;
@@ -66,7 +65,7 @@ public class GridDropHandler {
*/
public GridDropHandler(GridLayoutRule gridLayoutRule, INode layout, Object view) {
mRule = gridLayoutRule;
- mGrid = new GridModel(mRule.getRulesEngine(), layout, view);
+ mGrid = GridModel.get(mRule.getRulesEngine(), layout, view);
}
/**
@@ -522,9 +521,7 @@ public class GridDropHandler {
int rowSpan = endRow - row + 1;
// Make sure my math was right:
- if (mRowMatch.type == SegmentType.BASELINE) {
- assert rowSpan == 1 : rowSpan;
- }
+ 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
@@ -588,9 +585,6 @@ public class GridDropHandler {
if (insertMarginColumn) {
column++;
}
- // TODO: This grid refresh is a little risky because we may have added a new
- // child (spacer) which has no bounds yet!
- mGrid.loadFromXml();
}
// Split cells to make a new row
@@ -628,7 +622,6 @@ public class GridDropHandler {
if (insertMarginRow) {
row++;
}
- mGrid.loadFromXml();
}
// Figure out where to insert the new child
@@ -649,22 +642,33 @@ public class GridDropHandler {
next.applyPositionAttributes();
}
- // Set the cell position of the new widget
+ // Set the cell position (gravity) of the new widget
+ int gravity = 0;
if (mColumnMatch.type == SegmentType.RIGHT) {
- mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, VALUE_RIGHT);
+ gravity |= GravityHelper.GRAVITY_RIGHT;
} else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) {
- mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, VALUE_CENTER_HORIZONTAL);
+ gravity |= GravityHelper.GRAVITY_CENTER_HORIZ;
}
mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, column);
if (mRowMatch.type == SegmentType.BOTTOM) {
- String value = VALUE_BOTTOM;
- if (mColumnMatch.type == SegmentType.RIGHT) {
- value = value + '|' + VALUE_RIGHT;
- } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) {
- value = value + '|' + VALUE_CENTER_HORIZONTAL;
- }
- mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, value);
+ 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;
+ }
+ if (!GravityHelper.isConstrainedVertically(gravity)) {
+ 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
@@ -700,7 +704,6 @@ public class GridDropHandler {
newChild, UNDEFINED, false, UNDEFINED, UNDEFINED);
}
if (mRowMatch.createCell) {
- mGrid.loadFromXml();
mGrid.addRow(mRowMatch.cellIndex, newChild, UNDEFINED, false, UNDEFINED, UNDEFINED);
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridLayoutPainter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridLayoutPainter.java
index 461ca2b..9f68afb 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridLayoutPainter.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridLayoutPainter.java
@@ -234,15 +234,27 @@ public class GridLayoutPainter {
gc.drawLine(x, b.y, x, b.y2());
}
- // Draw preview rectangle of the first dragged element
+ // Draw preview rectangles for all the dragged elements
gc.useStyle(DrawingStyle.DROP_PREVIEW);
- mRule.drawElement(gc, first, x + offsetX - bounds.x, y + offsetY - bounds.y);
-
- // Preview baseline as well
- if (feedback.dragBaseline != -1) {
- int x1 = dragBounds.x + x + offsetX - bounds.x;
- int y1 = dragBounds.y + y + offsetY - bounds.y + feedback.dragBaseline;
- gc.drawLine(x1, y1, x1 + dragBounds.w, y1);
+ offsetX += x - bounds.x;
+ offsetY += y - bounds.y;
+
+ for (IDragElement element : mElements) {
+ if (element == first) {
+ mRule.drawElement(gc, first, offsetX, offsetY);
+ // Preview baseline as well
+ if (feedback.dragBaseline != -1) {
+ int x1 = dragBounds.x + offsetX;
+ int y1 = dragBounds.y + offsetY + feedback.dragBaseline;
+ gc.drawLine(x1, y1, x1 + dragBounds.w, y1);
+ }
+ } else {
+ b = element.getBounds();
+ if (b.isValid()) {
+ gc.drawRect(b.x + offsetX, b.y + offsetY,
+ b.x + offsetX + b.w, b.y + offsetY + b.h);
+ }
+ }
}
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridMatch.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridMatch.java
index aaca4bc..186e7d0 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridMatch.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridMatch.java
@@ -88,39 +88,39 @@ class GridMatch implements Comparable<GridMatch> {
public String getDisplayName(INode layout) {
switch (type) {
case BASELINE:
- return String.format("Align baseline in row %1$d", cellIndex);
+ return String.format("Align baseline in row %1$d", cellIndex + 1);
case CENTER_HORIZONTAL:
return "Center horizontally";
case LEFT:
if (!createCell) {
- return String.format("Insert into column %1$d", cellIndex);
+ return String.format("Insert into column %1$d", cellIndex + 1);
}
if (margin != UNDEFINED) {
if (cellIndex == 0 && margin != 0) {
return "Add one margin distance from the left";
}
- return String.format("Add next to column %1$d", cellIndex);
+ return String.format("Add next to column %1$d", cellIndex + 1);
}
return String.format("Align left at x=%1$d", matchedLine - layout.getBounds().x);
case RIGHT:
if (!createCell) {
- return String.format("Insert right-aligned into column %1$d", cellIndex);
+ return String.format("Insert right-aligned into column %1$d", cellIndex + 1);
}
return String.format("Align right at x=%1$d", matchedLine - layout.getBounds().x);
case TOP:
if (!createCell) {
- return String.format("Insert into row %1$d", cellIndex);
+ return String.format("Insert into row %1$d", cellIndex + 1);
}
if (margin != UNDEFINED) {
if (cellIndex == 0 && margin != 0) {
return "Add one margin distance from the top";
}
- return String.format("Add below row %1$d", cellIndex);
+ return String.format("Add below row %1$d", cellIndex + 1);
}
return String.format("Align top at y=%1d", matchedLine - layout.getBounds().y);
case BOTTOM:
if (!createCell) {
- return String.format("Insert into bottom of row %1$d", cellIndex);
+ return String.format("Insert into bottom of row %1$d", cellIndex + 1);
}
return String.format("Align bottom at y=%1d", matchedLine - layout.getBounds().y);
case CENTER_VERTICAL:
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridModel.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridModel.java
index 1d17c9b..65a61b4 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridModel.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridModel.java
@@ -19,7 +19,6 @@ import static com.android.ide.common.layout.GravityHelper.GRAVITY_BOTTOM;
import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_HORIZ;
import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_VERT;
import static com.android.ide.common.layout.GravityHelper.GRAVITY_RIGHT;
-import static com.android.util.XmlUtils.ANDROID_URI;
import static com.android.ide.common.layout.LayoutConstants.ATTR_COLUMN_COUNT;
import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN;
@@ -42,10 +41,13 @@ import static com.android.ide.common.layout.LayoutConstants.VALUE_CENTER_VERTICA
import static com.android.ide.common.layout.LayoutConstants.VALUE_N_DP;
import static com.android.ide.common.layout.LayoutConstants.VALUE_TOP;
import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL;
+import static com.android.util.XmlUtils.ANDROID_URI;
import static java.lang.Math.abs;
import static java.lang.Math.max;
import static java.lang.Math.min;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
import com.android.ide.common.api.IClientRulesEngine;
import com.android.ide.common.api.INode;
import com.android.ide.common.api.IViewMetadata;
@@ -54,10 +56,14 @@ import com.android.ide.common.api.Rect;
import com.android.ide.common.layout.GravityHelper;
import com.android.ide.common.layout.GridLayoutRule;
import com.android.util.Pair;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -67,8 +73,6 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
/** Models a GridLayout */
public class GridModel {
@@ -77,25 +81,31 @@ public class GridModel {
/** The size of spacers in the dimension that they are not defining */
private static final int SPACER_SIZE_DP = 1;
+
/** Attribute value used for {@link #SPACER_SIZE_DP} */
private static final String SPACER_SIZE = String.format(VALUE_N_DP, SPACER_SIZE_DP);
+
/** Width assigned to a newly added column with the Add Column action */
private static final int DEFAULT_CELL_WIDTH = 100;
+
/** Height assigned to a newly added row with the Add Row action */
private static final int DEFAULT_CELL_HEIGHT = 15;
- private static final Pattern DIP_PATTERN = Pattern.compile("(\\d+)dp"); //$NON-NLS-1$
/** The GridLayout node, never null */
public final INode layout;
/** True if this is a vertical layout, and false if it is horizontal (the default) */
public boolean vertical;
+
/** The declared count of rows (which may be {@link #UNDEFINED} if not specified) */
public int declaredRowCount;
+
/** The declared count of columns (which may be {@link #UNDEFINED} if not specified) */
public int declaredColumnCount;
+
/** The actual count of rows found in the grid */
public int actualRowCount;
+
/** The actual count of columns found in the grid */
public int actualColumnCount;
@@ -137,15 +147,6 @@ public class GridModel {
/** The {@link IClientRulesEngine} */
private final IClientRulesEngine mRulesEngine;
- /** List of nodes marked for deletion (may be null) */
- private Set<INode> mDeleted;
-
- /**
- * Flag which tracks whether we've edited the DOM model, in which case the grid data
- * may be stale and should be refreshed.
- */
- private boolean stale;
-
/**
* An actual instance of a GridLayout object that this grid model corresponds to.
*/
@@ -161,13 +162,44 @@ public class GridModel {
* @param node the GridLayout node
* @param viewObject an actual GridLayout instance, or null
*/
- public GridModel(IClientRulesEngine rulesEngine, INode node, Object viewObject) {
+ private GridModel(IClientRulesEngine rulesEngine, INode node, Object viewObject) {
mRulesEngine = rulesEngine;
layout = node;
mViewObject = viewObject;
loadFromXml();
}
+ // Factory cache for most recent item (used primarily because during paints and drags
+ // the grid model is called repeatedly for the same view object.)
+ private static WeakReference<Object> sCachedViewObject = new WeakReference<Object>(null);
+ private static WeakReference<GridModel> sCachedViewModel;
+
+ /**
+ * Factory which returns a grid model for the given node.
+ *
+ * @param rulesEngine the associated rules engine
+ * @param node the GridLayout node
+ * @param viewObject an actual GridLayout instance, or null
+ * @return a new model
+ */
+ @NonNull
+ public static GridModel get(
+ @NonNull IClientRulesEngine rulesEngine,
+ @NonNull INode node,
+ @Nullable Object viewObject) {
+ if (viewObject != null && viewObject == sCachedViewObject.get()) {
+ GridModel model = sCachedViewModel.get();
+ if (model != null) {
+ return model;
+ }
+ }
+
+ GridModel model = new GridModel(rulesEngine, node, viewObject);
+ sCachedViewModel = new WeakReference<GridModel>(model);
+ sCachedViewObject = new WeakReference<Object>(viewObject);
+ return model;
+ }
+
/**
* Returns the {@link ViewData} for the child at the given index
*
@@ -365,7 +397,7 @@ public class GridModel {
/**
* Loads a {@link GridModel} from the XML model.
*/
- void loadFromXml() {
+ private void loadFromXml() {
INode[] children = layout.getChildren();
declaredRowCount = getGridAttribute(layout, ATTR_ROW_COUNT, UNDEFINED);
@@ -381,17 +413,17 @@ public class GridModel {
}
// Assign row/column positions to all cells that do not explicitly define them
- assignRowsAndColumns(
- declaredRowCount == UNDEFINED ? children.length : declaredRowCount,
- declaredColumnCount == UNDEFINED ? children.length : declaredColumnCount);
+ if (!assignRowsAndColumnsFromViews(mChildViews)) {
+ assignRowsAndColumnsFromXml(
+ declaredRowCount == UNDEFINED ? children.length : declaredRowCount,
+ declaredColumnCount == UNDEFINED ? children.length : declaredColumnCount);
+ }
assignCellBounds();
for (int i = 0; i <= actualRowCount; i++) {
mBaselines[i] = UNDEFINED;
}
-
- stale = false;
}
private Pair<Map<Integer, Integer>, Map<Integer, Integer>> findCellsOutsideDeclaredBounds() {
@@ -450,7 +482,7 @@ public class GridModel {
* TODO: Consolidate with the algorithm in GridLayout to ensure we get the
* exact same results!
*/
- private void assignRowsAndColumns(int rowCount, int columnCount) {
+ private void assignRowsAndColumnsFromXml(int rowCount, int columnCount) {
Pair<Map<Integer, Integer>, Map<Integer, Integer>> p = findCellsOutsideDeclaredBounds();
Map<Integer, Integer> extraRowsMap = p.getFirst();
Map<Integer, Integer> extraColumnsMap = p.getSecond();
@@ -553,6 +585,80 @@ public class GridModel {
}
}
+ private static boolean sAttemptSpecReflection = true;
+
+ private boolean assignRowsAndColumnsFromViews(List<ViewData> views) {
+ if (!sAttemptSpecReflection) {
+ return false;
+ }
+
+ try {
+ // Lazily initialized reflection methods
+ Field spanField = null;
+ Field rowSpecField = null;
+ Field colSpecField = null;
+ Field minField = null;
+ Field maxField = null;
+ Method getLayoutParams = null;
+
+ for (ViewData view : views) {
+ // TODO: If the element *specifies* anything in XML, use that instead
+ Object child = mRulesEngine.getViewObject(view.node);
+ if (child == null) {
+ // Fallback to XML model
+ return false;
+ }
+
+ if (getLayoutParams == null) {
+ getLayoutParams = child.getClass().getMethod("getLayoutParams"); //$NON-NLS-1$
+ }
+ Object layoutParams = getLayoutParams.invoke(child);
+ if (rowSpecField == null) {
+ Class<? extends Object> layoutParamsClass = layoutParams.getClass();
+ rowSpecField = layoutParamsClass.getDeclaredField("rowSpec"); //$NON-NLS-1$
+ colSpecField = layoutParamsClass.getDeclaredField("columnSpec"); //$NON-NLS-1$
+ rowSpecField.setAccessible(true);
+ colSpecField.setAccessible(true);
+ }
+ assert colSpecField != null;
+
+ Object rowSpec = rowSpecField.get(layoutParams);
+ Object colSpec = colSpecField.get(layoutParams);
+ if (spanField == null) {
+ spanField = rowSpec.getClass().getDeclaredField("span"); //$NON-NLS-1$
+ spanField.setAccessible(true);
+ }
+ assert spanField != null;
+ Object rowInterval = spanField.get(rowSpec);
+ Object colInterval = spanField.get(colSpec);
+ if (minField == null) {
+ Class<? extends Object> intervalClass = rowInterval.getClass();
+ minField = intervalClass.getDeclaredField("min"); //$NON-NLS-1$
+ maxField = intervalClass.getDeclaredField("max"); //$NON-NLS-1$
+ minField.setAccessible(true);
+ maxField.setAccessible(true);
+ }
+ assert maxField != null;
+
+ int row = minField.getInt(rowInterval);
+ int col = minField.getInt(colInterval);
+ int rowEnd = maxField.getInt(rowInterval);
+ int colEnd = maxField.getInt(colInterval);
+
+ view.column = col;
+ view.row = row;
+ view.columnSpan = colEnd - col;
+ view.rowSpan = rowEnd - row;
+ }
+
+ return true;
+
+ } catch (Throwable e) {
+ sAttemptSpecReflection = false;
+ return false;
+ }
+ }
+
/**
* Computes the positions of the column and row boundaries
*/
@@ -805,10 +911,8 @@ public class GridModel {
*/
public INode addColumn(int newColumn, INode newView, int columnWidthDp,
boolean split, int row, int x) {
- assert !stale;
- stale = true;
-
// Insert a new column
+ actualColumnCount++;
if (declaredColumnCount != UNDEFINED) {
declaredColumnCount++;
setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount);
@@ -838,11 +942,14 @@ public class GridModel {
index++;
}
- newView = addSpacer(layout, index,
+ ViewData newViewData = addSpacer(layout, index,
split ? row : UNDEFINED,
split ? newColumn - 1 : UNDEFINED,
columnWidthDp != UNDEFINED ? columnWidthDp : DEFAULT_CELL_WIDTH,
DEFAULT_CELL_HEIGHT);
+ newViewData.column = newColumn - 1;
+ newViewData.row = row;
+ newView = newViewData.node;
}
// Set the actual row number on the first cell on the new row.
@@ -850,19 +957,23 @@ public class GridModel {
// the new row number, but we use the spacer to assign the row
// some height.
if (view.column == newColumn) {
- setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column + 1);
+ view.column++;
+ setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column);
} // else: endColumn == newColumn: handled below
} else if (getGridAttribute(view.node, ATTR_LAYOUT_COLUMN) != null) {
- setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column + 1);
+ view.column++;
+ setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column);
}
} else if (endColumn > newColumn) {
- setColumnSpanAttribute(view.node, view.columnSpan + 1);
+ view.columnSpan++;
+ setColumnSpanAttribute(view.node, view.columnSpan);
columnSpanSet = true;
}
if (split && !columnSpanSet && view.node.getBounds().x2() > x) {
if (view.node.getBounds().x < x) {
- setColumnSpanAttribute(view.node, view.columnSpan + 1);
+ view.columnSpan++;
+ setColumnSpanAttribute(view.node, view.columnSpan);
}
}
}
@@ -896,18 +1007,17 @@ public class GridModel {
return;
}
- assert !stale;
- stale = true;
-
// Figure out which columns should be removed
- Set<Integer> removedSet = new HashSet<Integer>();
+ Set<Integer> removeColumns = new HashSet<Integer>();
+ Set<ViewData> removedViews = new HashSet<ViewData>();
for (INode child : selectedChildren) {
ViewData view = getView(child);
- removedSet.add(view.column);
+ removedViews.add(view);
+ removeColumns.add(view.column);
}
// Sort them in descending order such that we can process each
// deletion independently
- List<Integer> removed = new ArrayList<Integer>(removedSet);
+ List<Integer> removed = new ArrayList<Integer>(removeColumns);
Collections.sort(removed, Collections.reverseOrder());
for (int removedColumn : removed) {
@@ -916,9 +1026,9 @@ public class GridModel {
// TODO: Don't do this if the column being deleted is outside
// the declared column range!
// TODO: Do this under a write lock? / editXml lock?
+ actualColumnCount--;
if (declaredColumnCount != UNDEFINED) {
declaredColumnCount--;
- setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount);
}
// Remove any elements that begin in the deleted columns...
@@ -935,21 +1045,44 @@ public class GridModel {
int columnWidth = getColumnWidth(removedColumn, view.columnSpan) -
getColumnWidth(removedColumn, 1);
int columnWidthDip = mRulesEngine.pxToDp(columnWidth);
- addSpacer(layout, index, UNDEFINED, UNDEFINED, columnWidthDip,
- SPACER_SIZE_DP);
+ ViewData spacer = addSpacer(layout, index, UNDEFINED, UNDEFINED,
+ columnWidthDip, SPACER_SIZE_DP);
+ spacer.row = 0;
+ spacer.column = removedColumn;
}
layout.removeChild(view.node);
} else if (view.column < removedColumn
&& view.column + view.columnSpan > removedColumn) {
// Subtract column span to skip this item
- setColumnSpanAttribute(view.node, view.columnSpan - 1);
+ view.columnSpan--;
+ setColumnSpanAttribute(view.node, view.columnSpan);
} else if (view.column > removedColumn) {
+ view.column--;
if (getGridAttribute(view.node, ATTR_LAYOUT_COLUMN) != null) {
- setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column - 1);
+ setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column);
}
}
}
}
+
+ // Remove children from child list!
+ if (removedViews.size() <= 2) {
+ mChildViews.removeAll(removedViews);
+ } else {
+ List<ViewData> remaining =
+ new ArrayList<ViewData>(mChildViews.size() - removedViews.size());
+ for (ViewData view : mChildViews) {
+ if (!removedViews.contains(view)) {
+ remaining.add(view);
+ }
+ }
+ mChildViews = remaining;
+ }
+
+ //if (declaredColumnCount != UNDEFINED) {
+ setGridAttribute(layout, ATTR_COLUMN_COUNT, actualColumnCount);
+ //}
+
}
/**
@@ -991,14 +1124,12 @@ public class GridModel {
*/
public INode addRow(int newRow, INode newView, int rowHeightDp, boolean split,
int column, int y) {
- // We'll modify the grid data; the cached data is out of date
- assert !stale;
- stale = true;
-
+ actualRowCount++;
if (declaredRowCount != UNDEFINED) {
declaredRowCount++;
setGridAttribute(layout, ATTR_ROW_COUNT, declaredRowCount);
}
+
boolean added = false;
for (ViewData view : mChildViews) {
if (view.row >= newRow) {
@@ -1011,30 +1142,37 @@ public class GridModel {
if (declaredColumnCount != UNDEFINED && !split) {
setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount);
}
- newView = addSpacer(layout, index,
+ ViewData newViewData = addSpacer(layout, index,
split ? newRow - 1 : UNDEFINED,
split ? column : UNDEFINED,
SPACER_SIZE_DP,
rowHeightDp != UNDEFINED ? rowHeightDp : DEFAULT_CELL_HEIGHT);
+ newViewData.column = column;
+ newViewData.row = newRow - 1;
+ newView = newViewData.node;
}
// Set the actual row number on the first cell on the new row.
// This means we don't really need the spacer above to imply
// the new row number, but we use the spacer to assign the row
// some height.
- setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row + 1);
+ view.row++;
+ setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row);
added = true;
} else if (getGridAttribute(view.node, ATTR_LAYOUT_ROW) != null) {
- setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row + 1);
+ view.row++;
+ setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row);
}
} else {
int endRow = view.row + view.rowSpan;
if (endRow > newRow) {
- setRowSpanAttribute(view.node, view.rowSpan + 1);
+ view.rowSpan++;
+ setRowSpanAttribute(view.node, view.rowSpan);
} else if (split && view.node.getBounds().y2() > y) {
if (view.node.getBounds().y < y) {
- setRowSpanAttribute(view.node, view.rowSpan + 1);
+ view.rowSpan++;
+ setRowSpanAttribute(view.node, view.rowSpan);
}
}
}
@@ -1043,9 +1181,13 @@ public class GridModel {
if (!added) {
// Append a row at the end
if (newView == null) {
- newView = addSpacer(layout, -1, UNDEFINED, UNDEFINED,
+ ViewData newViewData = addSpacer(layout, -1, UNDEFINED, UNDEFINED,
SPACER_SIZE_DP,
rowHeightDp != UNDEFINED ? rowHeightDp : DEFAULT_CELL_HEIGHT);
+ newViewData.column = column;
+ // TODO: MAke sure this row number is right!
+ newViewData.row = split ? newRow - 1 : newRow;
+ newView = newViewData.node;
}
if (declaredColumnCount != UNDEFINED && !split) {
setGridAttribute(layout, ATTR_COLUMN_COUNT, declaredColumnCount);
@@ -1053,7 +1195,6 @@ public class GridModel {
if (split) {
setGridAttribute(newView, ATTR_LAYOUT_ROW, newRow - 1);
setGridAttribute(newView, ATTR_LAYOUT_COLUMN, column);
-
}
}
@@ -1070,18 +1211,17 @@ public class GridModel {
return;
}
- assert !stale;
- stale = true;
-
// Figure out which rows should be removed
- Set<Integer> removedSet = new HashSet<Integer>();
+ Set<ViewData> removedViews = new HashSet<ViewData>();
+ Set<Integer> removedRows = new HashSet<Integer>();
for (INode child : selectedChildren) {
ViewData view = getView(child);
- removedSet.add(view.row);
+ removedViews.add(view);
+ removedRows.add(view.row);
}
// Sort them in descending order such that we can process each
// deletion independently
- List<Integer> removed = new ArrayList<Integer>(removedSet);
+ List<Integer> removed = new ArrayList<Integer>(removedRows);
Collections.sort(removed, Collections.reverseOrder());
for (int removedRow : removed) {
@@ -1089,6 +1229,7 @@ public class GridModel {
// First, adjust row count.
// TODO: Don't do this if the row being deleted is outside
// the declared row range!
+ actualRowCount--;
if (declaredRowCount != UNDEFINED) {
declaredRowCount--;
setGridAttribute(layout, ATTR_ROW_COUNT, declaredRowCount);
@@ -1104,17 +1245,34 @@ public class GridModel {
// We don't have to worry about a rowSpan > 1 here, because even
// if it is, those rowspans are not used to assign default row/column
// positions for other cells
+// TODO: Check this; it differs from the removeColumns logic!
layout.removeChild(view.node);
} else if (view.row > removedRow) {
+ view.row--;
if (getGridAttribute(view.node, ATTR_LAYOUT_ROW) != null) {
- setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row - 1);
+ setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row);
}
} else if (view.row < removedRow
&& view.row + view.rowSpan > removedRow) {
// Subtract row span to skip this item
- setRowSpanAttribute(view.node, view.rowSpan - 1);
+ view.rowSpan--;
+ setRowSpanAttribute(view.node, view.rowSpan);
+ }
+ }
+ }
+
+ // Remove children from child list!
+ if (removedViews.size() <= 2) {
+ mChildViews.removeAll(removedViews);
+ } else {
+ List<ViewData> remaining =
+ new ArrayList<ViewData>(mChildViews.size() - removedViews.size());
+ for (ViewData view : mChildViews) {
+ if (!removedViews.contains(view)) {
+ remaining.add(view);
}
}
+ mChildViews = remaining;
}
}
@@ -1364,10 +1522,6 @@ public class GridModel {
*/
@Override
public String toString() {
- if (stale) {
- System.out.println("WARNING: Grid has been modified, so model may be out of date!");
- }
-
// Dump out the view table
int cellWidth = 25;
@@ -1380,9 +1534,6 @@ public class GridModel {
rowList.add(columnList);
}
for (ViewData view : mChildViews) {
- if (mDeleted != null && mDeleted.contains(view.node)) {
- continue;
- }
for (int i = 0; i < view.rowSpan; i++) {
if (view.row + i > mTop.length) { // Guard against bogus span values
break;
@@ -1456,8 +1607,7 @@ public class GridModel {
* @param x the x coordinate of the new column
*/
public void splitColumn(int newColumn, boolean insertMarginColumn, int columnWidthDp, int x) {
- assert !stale;
- stale = true;
+ actualColumnCount++;
// Insert a new column
if (declaredColumnCount != UNDEFINED) {
@@ -1525,14 +1675,24 @@ public class GridModel {
// skipped column!
//if (getGridAttribute(node, ATTR_LAYOUT_COLUMN) != null) {
- setGridAttribute(node, ATTR_LAYOUT_COLUMN, column + (insertMarginColumn ? 2 : 1));
+ view.column += insertMarginColumn ? 2 : 1;
+ setGridAttribute(node, ATTR_LAYOUT_COLUMN, view.column);
//}
} else if (!view.isSpacer()) {
+ // Adjust the column span? We must increase it if
+ // (1) the new column is inside the range [column, column + columnSpan]
+ // (2) the new column is within the last cell in the column span,
+ // and the exact X location of the split is within the horizontal
+ // *bounds* of this node (provided it has gravity=left)
+ // (3) the new column is within the last cell and the cell has gravity
+ // right or gravity center
int endColumn = column + view.columnSpan;
if (endColumn > newColumn
- || endColumn == newColumn && view.node.getBounds().x2() > x) {
+ || endColumn == newColumn && (view.node.getBounds().x2() > x
+ || !GravityHelper.isLeftAligned(view.gravity))) {
// This cell spans the new insert position, so increment the column span
- setColumnSpanAttribute(node, view.columnSpan + (insertMarginColumn ? 2 : 1));
+ view.columnSpan += insertMarginColumn ? 2 : 1;
+ setColumnSpanAttribute(node, view.columnSpan);
}
}
}
@@ -1548,8 +1708,9 @@ public class GridModel {
if (remaining > 0) {
prevColumnSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH,
String.format(VALUE_N_DP, remaining));
+ prevColumnSpacer.column = insertMarginColumn ? newColumn + 1 : newColumn;
setGridAttribute(prevColumnSpacer.node, ATTR_LAYOUT_COLUMN,
- insertMarginColumn ? newColumn + 1 : newColumn);
+ prevColumnSpacer.column);
}
}
@@ -1575,6 +1736,8 @@ public class GridModel {
* @param y the y coordinate of the new row
*/
public void splitRow(int newRow, boolean insertMarginRow, int rowHeightDp, int y) {
+ actualRowCount++;
+
// Insert a new row
if (declaredRowCount != UNDEFINED) {
declaredRowCount++;
@@ -1603,14 +1766,17 @@ public class GridModel {
int row = view.row;
if (row > newRow || (row == newRow && view.node.getBounds().y2() > y)) {
//if (getGridAttribute(node, ATTR_LAYOUT_ROW) != null) {
- setGridAttribute(node, ATTR_LAYOUT_ROW, row + (insertMarginRow ? 2 : 1));
+ view.row += insertMarginRow ? 2 : 1;
+ setGridAttribute(node, ATTR_LAYOUT_ROW, view.row);
//}
} else if (!view.isSpacer()) {
int endRow = row + view.rowSpan;
if (endRow > newRow
- || endRow == newRow && view.node.getBounds().y2() > y) {
+ || endRow == newRow && (view.node.getBounds().y2() > y
+ || !GravityHelper.isTopAligned(view.gravity))) {
// This cell spans the new insert position, so increment the row span
- setRowSpanAttribute(node, view.rowSpan + (insertMarginRow ? 2 : 1));
+ view.rowSpan += insertMarginRow ? 2 : 1;
+ setRowSpanAttribute(node, view.rowSpan);
}
}
}
@@ -1626,8 +1792,8 @@ public class GridModel {
if (remaining > 0) {
prevRowSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT,
String.format(VALUE_N_DP, remaining));
- setGridAttribute(prevRowSpacer.node, ATTR_LAYOUT_ROW,
- insertMarginRow ? newRow + 1 : newRow);
+ prevRowSpacer.row = insertMarginRow ? newRow + 1 : newRow;
+ setGridAttribute(prevRowSpacer.node, ATTR_LAYOUT_ROW, prevRowSpacer.row);
}
}
@@ -1664,12 +1830,8 @@ public class GridModel {
/** Applies the column and row fields into the XML model */
void applyPositionAttributes() {
- if (getGridAttribute(node, ATTR_LAYOUT_COLUMN) == null) {
- setGridAttribute(node, ATTR_LAYOUT_COLUMN, column);
- }
- if (getGridAttribute(node, ATTR_LAYOUT_ROW) == null) {
- setGridAttribute(node, ATTR_LAYOUT_ROW, row);
- }
+ setGridAttribute(node, ATTR_LAYOUT_COLUMN, column);
+ setGridAttribute(node, ATTR_LAYOUT_ROW, row);
}
/** Returns the id of this node, or makes one up for display purposes */
@@ -1688,8 +1850,7 @@ public class GridModel {
/** Returns true if this {@link ViewData} represents a spacer */
boolean isSpacer() {
- String fqcn = node.getFqcn();
- return FQCN_SPACE.equals(fqcn) || FQCN_SPACE_V7.equals(fqcn);
+ return isSpace(node.getFqcn());
}
/**
@@ -1755,46 +1916,52 @@ public class GridModel {
}
/**
- * Notify the grid that the given node is about to be deleted. This can be used in
- * conjunction with {@link #cleanup()} to remove and merge unnecessary rows and
- * columns.
+ * Update the model to account for the given nodes getting deleted. The nodes
+ * are not actually deleted by this method; that is assumed to be performed by the
+ * caller. Instead this method performs whatever model updates are necessary to
+ * preserve the grid structure.
*
- * @param child the child that is going to be removed shortly
+ * @param nodes the nodes to be deleted
*/
- public void markDeleted(INode child) {
- if (mDeleted == null) {
- mDeleted = new HashSet<INode>();
+ public void onDeleted(@NonNull List<INode> nodes) {
+ if (nodes.size() == 0) {
+ return;
}
- mDeleted.add(child);
- }
+ // Attempt to clean up spacer objects for any newly-empty rows or columns
+ // as the result of this deletion
- /**
- * Clean up rows and columns that are no longer needed after the nodes marked for
- * deletion by {@link #markDeleted(INode)} are removed.
- */
- public void cleanup() {
- if (mDeleted == null) {
- return;
+ Set<INode> deleted = new HashSet<INode>();
+
+ for (INode child : nodes) {
+ // We don't care about deletion of spacers
+ String fqcn = child.getFqcn();
+ if (fqcn.equals(FQCN_SPACE) || fqcn.equals(FQCN_SPACE_V7)) {
+ continue;
+ }
+ deleted.add(child);
}
Set<Integer> usedColumns = new HashSet<Integer>(actualColumnCount);
- Set<Integer> usedRows = new HashSet<Integer>(actualColumnCount);
- Map<Integer, ViewData> columnSpacers = new HashMap<Integer, ViewData>(actualColumnCount);
- Map<Integer, ViewData> rowSpacers = new HashMap<Integer, ViewData>(actualColumnCount);
+ Set<Integer> usedRows = new HashSet<Integer>(actualRowCount);
+ Multimap<Integer, ViewData> columnSpacers = ArrayListMultimap.create(actualColumnCount, 2);
+ Multimap<Integer, ViewData> rowSpacers = ArrayListMultimap.create(actualRowCount, 2);
+ Set<ViewData> removedViews = new HashSet<ViewData>();
for (ViewData view : mChildViews) {
- if (view.isColumnSpacer()) {
+ if (deleted.contains(view.node)) {
+ removedViews.add(view);
+ } else if (view.isColumnSpacer()) {
columnSpacers.put(view.column, view);
} else if (view.isRowSpacer()) {
rowSpacers.put(view.row, view);
- } else if (!mDeleted.contains(view.node)) {
+ } else {
usedColumns.add(Integer.valueOf(view.column));
usedRows.add(Integer.valueOf(view.row));
}
}
- if (usedColumns.size() == 0) {
+ if (usedColumns.size() == 0 || usedRows.size() == 0) {
// No more views - just remove all the spacers
for (ViewData spacer : columnSpacers.values()) {
layout.removeChild(spacer.node);
@@ -1802,158 +1969,263 @@ public class GridModel {
for (ViewData spacer : rowSpacers.values()) {
layout.removeChild(spacer.node);
}
+ mChildViews.clear();
+ actualColumnCount = 0;
+ declaredColumnCount = 2;
+ actualRowCount = 0;
+ declaredRowCount = UNDEFINED;
setGridAttribute(layout, ATTR_COLUMN_COUNT, 2);
return;
}
- // Remove (merge back) unnecessary columns
- for (int column = actualColumnCount - 1; column >= 0; column--) {
- if (!usedColumns.contains(column)) {
- // This column is no longer needed. Remove it!
- ViewData spacer = columnSpacers.get(column);
- ViewData prevSpacer = columnSpacers.get(column - 1);
- if (spacer == null) {
- // Can't touch this column; we only merge spacer columns, not
- // other types of columns (TODO: Consider what we can do here!)
-
- // Try to merge with next column
- ViewData nextSpacer = columnSpacers.get(column + 1);
- if (nextSpacer != null) {
- int nextSizeDp = getDipSize(nextSpacer, false /* row */);
- int columnWidthPx = getColumnWidth(column, 1);
- int columnWidthDp = mRulesEngine.pxToDp(columnWidthPx);
- int combinedSizeDp = nextSizeDp + columnWidthDp;
- nextSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH,
- String.format(VALUE_N_DP, combinedSizeDp));
- // Also move the spacer into this column
- setGridAttribute(nextSpacer.node, ATTR_LAYOUT_COLUMN, column);
- columnSpacers.put(column, nextSpacer);
- } else {
- continue;
- }
- } else if (prevSpacer == null) {
- // Can't combine this column with a previous column; we don't have
- // data for it.
- continue;
- }
+ // Determine columns to introduce spacers into:
+ // This is tricky; I should NOT combine spacers if there are cells tied to
+ // individual ones
+
+ // TODO: Invalidate column sizes too! Otherwise repeated updates might get confused!
+ // Similarly, inserts need to do the same!
+
+ // Produce map of old column numbers to new column numbers
+ // Collapse regions of consecutive space and non-space ranges together
+ int[] columnMap = new int[actualColumnCount + 1]; // +1: Easily handle columnSpans as well
+ int newColumn = 0;
+ boolean prevUsed = usedColumns.contains(0);
+ for (int column = 1; column < actualColumnCount; column++) {
+ boolean used = usedColumns.contains(column);
+ if (used || prevUsed != used) {
+ newColumn++;
+ prevUsed = used;
+ }
+ columnMap[column] = newColumn;
+ }
+ newColumn++;
+ columnMap[actualColumnCount] = newColumn;
+ assert columnMap[0] == 0;
+
+ int[] rowMap = new int[actualRowCount + 1]; // +1: Easily handle rowSpans as well
+ int newRow = 0;
+ prevUsed = usedRows.contains(0);
+ for (int row = 1; row < actualRowCount; row++) {
+ boolean used = usedRows.contains(row);
+ if (used || prevUsed != used) {
+ newRow++;
+ prevUsed = used;
+ }
+ rowMap[row] = newRow;
+ }
+ newRow++;
+ rowMap[actualRowCount] = newRow;
+ assert rowMap[0] == 0;
- if (spacer != null) {
- // Combine spacer and prevSpacer.
- mergeSpacers(prevSpacer, spacer, false /*row*/);
+
+ // Adjust column and row numbers to account for deletions: for a given cell, if it
+ // is to the right of a deleted column, reduce its column number, and if it only
+ // spans across the deleted column, reduce its column span.
+ for (ViewData view : mChildViews) {
+ if (removedViews.contains(view)) {
+ continue;
+ }
+ int newColumnStart = columnMap[Math.min(columnMap.length - 1, view.column)];
+ // Gracefully handle rogue/invalid columnSpans in the XML
+ int newColumnEnd = columnMap[Math.min(columnMap.length - 1,
+ view.column + view.columnSpan)];
+ if (newColumnStart != view.column) {
+ view.column = newColumnStart;
+ setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column);
+ }
+
+ int columnSpan = newColumnEnd - newColumnStart;
+ if (columnSpan != view.columnSpan) {
+ if (columnSpan >= 1) {
+ view.columnSpan = columnSpan;
+ setColumnSpanAttribute(view.node, view.columnSpan);
+ } // else: merging spacing columns together
+ }
+
+
+ int newRowStart = rowMap[Math.min(rowMap.length - 1, view.row)];
+ int newRowEnd = rowMap[Math.min(rowMap.length - 1, view.row + view.rowSpan)];
+ if (newRowStart != view.row) {
+ view.row = newRowStart;
+ setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row);
+ }
+
+ int rowSpan = newRowEnd - newRowStart;
+ if (rowSpan != view.rowSpan) {
+ if (rowSpan >= 1) {
+ view.rowSpan = rowSpan;
+ setRowSpanAttribute(view.node, view.rowSpan);
+ } // else: merging spacing rows together
+ }
+ }
+
+ // Merge spacers (and add spacers for newly empty columns)
+ int start = 0;
+ while (start < actualColumnCount) {
+ // Find next unused span
+ while (start < actualColumnCount && usedColumns.contains(start)) {
+ start++;
+ }
+ if (start == actualColumnCount) {
+ break;
+ }
+ assert !usedColumns.contains(start);
+ // Find the next span of unused columns and produce a SINGLE
+ // spacer for that range (unless it's a zero-sized columns)
+ int end = start + 1;
+ for (; end < actualColumnCount; end++) {
+ if (usedColumns.contains(end)) {
+ break;
}
+ }
- // Decrement column numbers for all elements to the right of the deleted column,
- // and subtract columnSpans for any elements that overlap it
- for (ViewData view : mChildViews) {
- if (view.column >= column) {
- if (view.column > 0) {
- view.column--;
- setGridAttribute(view.node, ATTR_LAYOUT_COLUMN, view.column);
- }
- } else if (!view.isSpacer()) {
- int endColumn = view.column + view.columnSpan;
- if (endColumn > column && view.columnSpan > 1) {
- view.columnSpan--;
- setColumnSpanAttribute(view.node, view.columnSpan);
+ // Add up column sizes
+ int width = getColumnWidth(start, end - start);
+
+ // Find all spacers: the first one found should be moved to the start column
+ // and assigned to the full height of the columns, and
+ // the column count reduced by the corresponding amount
+
+ // TODO: if width = 0, fully remove
+
+ boolean isFirstSpacer = true;
+ for (int column = start; column < end; column++) {
+ Collection<ViewData> spacers = columnSpacers.get(column);
+ if (spacers != null && !spacers.isEmpty()) {
+ // Avoid ConcurrentModificationException since we're inserting into the
+ // map within this loop (always at a different index, but the map doesn't
+ // know that)
+ spacers = new ArrayList<ViewData>(spacers);
+ for (ViewData spacer : spacers) {
+ if (isFirstSpacer) {
+ isFirstSpacer = false;
+ spacer.column = columnMap[start];
+ setGridAttribute(spacer.node, ATTR_LAYOUT_COLUMN, spacer.column);
+ if (end - start > 1) {
+ // Compute a merged width for all the spacers (not needed if
+ // there's just one spacer; it should already have the correct width)
+ int columnWidthDp = mRulesEngine.pxToDp(width);
+ spacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH,
+ String.format(VALUE_N_DP, columnWidthDp));
+ }
+ columnSpacers.put(start, spacer);
+ } else {
+ removedViews.add(spacer); // Mark for model removal
+ layout.removeChild(spacer.node);
}
}
}
}
- }
- for (int row = actualRowCount - 1; row >= 0; row--) {
- if (!usedRows.contains(row)) {
- // This row is no longer needed. Remove it!
- ViewData spacer = rowSpacers.get(row);
- ViewData prevSpacer = rowSpacers.get(row - 1);
- if (spacer == null) {
- ViewData nextSpacer = rowSpacers.get(row + 1);
- if (nextSpacer != null) {
- int nextSizeDp = getDipSize(nextSpacer, true /* row */);
- int rowHeightPx = getRowHeight(row, 1);
- int rowHeightDp = mRulesEngine.pxToDp(rowHeightPx);
- int combinedSizeDp = nextSizeDp + rowHeightDp;
- nextSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT,
- String.format(VALUE_N_DP, combinedSizeDp));
- setGridAttribute(nextSpacer.node, ATTR_LAYOUT_ROW, row);
- rowSpacers.put(row, nextSpacer);
- } else {
- continue;
- }
- } else if (prevSpacer == null) {
- continue;
- }
+ if (isFirstSpacer) {
+ // No spacer: create one
+ int columnWidthDp = mRulesEngine.pxToDp(width);
+ addSpacer(layout, -1, UNDEFINED, columnMap[start], columnWidthDp, DEFAULT_CELL_HEIGHT);
+ }
- if (spacer != null) {
- // Combine spacer and prevSpacer.
- mergeSpacers(prevSpacer, spacer, true /*row*/);
- }
+ start = end;
+ }
+ actualColumnCount = newColumn;
+//if (usedColumns.contains(newColumn)) {
+// // TODO: This may be totally wrong for right aligned content!
+// actualColumnCount++;
+//}
+ // Merge spacers for rows
+ start = 0;
+ while (start < actualRowCount) {
+ // Find next unused span
+ while (start < actualRowCount && usedRows.contains(start)) {
+ start++;
+ }
+ if (start == actualRowCount) {
+ break;
+ }
+ assert !usedRows.contains(start);
+ // Find the next span of unused rows and produce a SINGLE
+ // spacer for that range (unless it's a zero-sized rows)
+ int end = start + 1;
+ for (; end < actualRowCount; end++) {
+ if (usedRows.contains(end)) {
+ break;
+ }
+ }
- // Decrement row numbers for all elements below the deleted row,
- // and subtract rowSpans for any elements that overlap it
- for (ViewData view : mChildViews) {
- if (view.row >= row) {
- if (view.row > 0) {
- view.row--;
- setGridAttribute(view.node, ATTR_LAYOUT_ROW, view.row);
- }
- } else if (!view.isSpacer()) {
- int endRow = view.row + view.rowSpan;
- if (endRow > row && view.rowSpan > 1) {
- view.rowSpan--;
- setRowSpanAttribute(view.node, view.rowSpan);
+ // Add up row sizes
+ int height = getRowHeight(start, end - start);
+
+ // Find all spacers: the first one found should be moved to the start row
+ // and assigned to the full height of the rows, and
+ // the row count reduced by the corresponding amount
+
+ // TODO: if width = 0, fully remove
+
+ boolean isFirstSpacer = true;
+ for (int row = start; row < end; row++) {
+ Collection<ViewData> spacers = rowSpacers.get(row);
+ if (spacers != null && !spacers.isEmpty()) {
+ // Avoid ConcurrentModificationException since we're inserting into the
+ // map within this loop (always at a different index, but the map doesn't
+ // know that)
+ spacers = new ArrayList<ViewData>(spacers);
+ for (ViewData spacer : spacers) {
+ if (isFirstSpacer) {
+ isFirstSpacer = false;
+ spacer.row = rowMap[start];
+ setGridAttribute(spacer.node, ATTR_LAYOUT_ROW, spacer.row);
+ if (end - start > 1) {
+ // Compute a merged width for all the spacers (not needed if
+ // there's just one spacer; it should already have the correct height)
+ int rowHeightDp = mRulesEngine.pxToDp(height);
+ spacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT,
+ String.format(VALUE_N_DP, rowHeightDp));
+ }
+ rowSpacers.put(start, spacer);
+ } else {
+ removedViews.add(spacer); // Mark for model removal
+ layout.removeChild(spacer.node);
}
}
}
}
- }
- // TODO: Reduce row/column counts!
- }
+ if (isFirstSpacer) {
+ // No spacer: create one
+ int rowWidthDp = mRulesEngine.pxToDp(height);
+ addSpacer(layout, -1, rowMap[start], UNDEFINED, DEFAULT_CELL_WIDTH, rowWidthDp);
+ }
- /**
- * Merges two spacers together - either row spacers or column spacers based on the
- * parameter
- */
- private void mergeSpacers(ViewData prevSpacer, ViewData spacer, boolean row) {
- int combinedSizeDp = -1;
- int prevSizeDp = getDipSize(prevSpacer, row);
- int sizeDp = getDipSize(spacer, row);
- combinedSizeDp = prevSizeDp + sizeDp;
- String attribute = row ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH;
- prevSpacer.node.setAttribute(ANDROID_URI, attribute,
- String.format(VALUE_N_DP, combinedSizeDp));
- layout.removeChild(spacer.node);
- }
+ start = end;
+ }
+ actualRowCount = newRow;
+// if (usedRows.contains(newRow)) {
+// actualRowCount++;
+// }
- /**
- * Computes the size (in device independent pixels) of the given spacer.
- *
- * @param spacer the spacer to measure
- * @param row if true, this is a row spacer, otherwise it is a column spacer
- * @return the size in device independent pixels
- */
- private int getDipSize(ViewData spacer, boolean row) {
- String attribute = row ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH;
- String size = spacer.node.getStringAttr(ANDROID_URI, attribute);
- if (size != null) {
- Matcher matcher = DIP_PATTERN.matcher(size);
- if (matcher.matches()) {
- try {
- return Integer.parseInt(matcher.group(1));
- } catch (NumberFormatException nfe) {
- // Can't happen; we pre-check with regexp above.
+ // Update the model: remove removed children from the view data list
+ if (removedViews.size() <= 2) {
+ mChildViews.removeAll(removedViews);
+ } else {
+ List<ViewData> remaining =
+ new ArrayList<ViewData>(mChildViews.size() - removedViews.size());
+ for (ViewData view : mChildViews) {
+ if (!removedViews.contains(view)) {
+ remaining.add(view);
}
}
+ mChildViews = remaining;
}
- // Fallback for cases where the attribute values are not regular (e.g. user has edited
- // to some resource or other dimension format) - in that case just do bounds-based
- // computation.
- Rect bounds = spacer.node.getBounds();
- return mRulesEngine.pxToDp(row ? bounds.h : bounds.w);
+ // Update the final column and row declared attributes
+ if (declaredColumnCount != UNDEFINED) {
+ declaredColumnCount = actualColumnCount;
+ setGridAttribute(layout, ATTR_COLUMN_COUNT, actualColumnCount);
+ }
+ if (declaredRowCount != UNDEFINED) {
+ declaredRowCount = actualRowCount;
+ setGridAttribute(layout, ATTR_ROW_COUNT, actualRowCount);
+ }
}
/**
@@ -1968,7 +2240,7 @@ public class GridModel {
* @param heightDp the height in device independent pixels to assign to the spacer
* @return the newly added spacer
*/
- INode addSpacer(INode parent, int index, int row, int column,
+ ViewData addSpacer(INode parent, int index, int row, int column,
int widthDp, int heightDp) {
INode spacer;
@@ -1984,10 +2256,15 @@ public class GridModel {
spacer = parent.appendChild(tag);
}
+ ViewData view = new ViewData(spacer, index != -1 ? index : mChildViews.size());
+ mChildViews.add(view);
+
if (row != UNDEFINED) {
+ view.row = row;
setGridAttribute(spacer, ATTR_LAYOUT_ROW, row);
}
if (column != UNDEFINED) {
+ view.column = column;
setGridAttribute(spacer, ATTR_LAYOUT_COLUMN, column);
}
if (widthDp > 0) {
@@ -2020,7 +2297,7 @@ public class GridModel {
}
- return spacer;
+ return view;
}
/**
@@ -2068,4 +2345,14 @@ public class GridModel {
public int getViewCount() {
return mChildViews.size();
}
+
+ /**
+ * Returns true if the given class name represents a spacer
+ *
+ * @param fqcn the fully qualified class name
+ * @return true if this is a spacer
+ */
+ public static boolean isSpace(String fqcn) {
+ return FQCN_SPACE.equals(fqcn) || FQCN_SPACE_V7.equals(fqcn);
+ }
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java
index 47159c3..bef070b 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java
@@ -19,6 +19,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
import static com.android.ide.common.layout.LayoutConstants.ATTR_COLUMN_COUNT;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN_SPAN;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_GRAVITY;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW_SPAN;
import static com.android.ide.common.layout.LayoutConstants.ATTR_ROW_COUNT;
@@ -30,6 +31,7 @@ import static com.android.ide.common.layout.LayoutConstants.LAYOUT_PREFIX;
import static com.android.tools.lint.detector.api.LintConstants.AUTO_URI;
import static com.android.tools.lint.detector.api.LintConstants.URI_PREFIX;
import static com.android.util.XmlUtils.ANDROID_URI;
+import static org.eclipse.jface.viewers.StyledString.COUNTER_STYLER;
import static org.eclipse.jface.viewers.StyledString.QUALIFIER_STYLER;
import com.android.annotations.VisibleForTesting;
@@ -800,11 +802,14 @@ public class OutlinePage extends ContentOutlinePage
// Temporary diagnostics code when developing GridLayout
if (GridLayoutRule.sDebugGridLayout) {
+
String namespace;
- if (e.getParentNode() != null
+ if (e.getNodeName().equals(GRID_LAYOUT) ||
+ e.getParentNode() != null
&& e.getParentNode().getNodeName().equals(GRID_LAYOUT)) {
namespace = ANDROID_URI;
} else {
+ // Else: probably a v7 gridlayout
IProject project = mGraphicalEditorPart.getProject();
ProjectState projectState = Sdk.getProjectState(project);
if (projectState != null && projectState.isLibrary()) {
@@ -872,6 +877,13 @@ public class OutlinePage extends ContentOutlinePage
styledString.append(',', QUALIFIER_STYLER);
styledString.append(rowSpan, QUALIFIER_STYLER);
styledString.append(')', QUALIFIER_STYLER);
+
+ String gravity = e.getAttributeNS(namespace, ATTR_LAYOUT_GRAVITY);
+ if (gravity != null && gravity.length() > 0) {
+ styledString.append(" : ", COUNTER_STYLER);
+ styledString.append(gravity, COUNTER_STYLER);
+ }
+
}
}
@@ -886,10 +898,14 @@ public class OutlinePage extends ContentOutlinePage
text = resolved;
}
}
- styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
- styledString.append('"', QUALIFIER_STYLER);
- styledString.append(truncate(text, styledString), QUALIFIER_STYLER);
- styledString.append('"', QUALIFIER_STYLER);
+ if (styledString.length() < LABEL_MAX_WIDTH - LABEL_SEPARATOR.length()
+ - 2) {
+ styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
+
+ styledString.append('"', QUALIFIER_STYLER);
+ styledString.append(truncate(text, styledString), QUALIFIER_STYLER);
+ styledString.append('"', QUALIFIER_STYLER);
+ }
}
} else if (e.hasAttributeNS(ANDROID_URI, ATTR_SRC)) {
// Show ImageView source attributes etc
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
index 23e42ef..424be26 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
@@ -16,6 +16,7 @@
package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_MERGE;
import com.android.annotations.NonNull;
@@ -31,6 +32,7 @@ import com.android.util.Pair;
import org.eclipse.swt.graphics.Rectangle;
import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
import org.w3c.dom.Node;
import java.awt.image.BufferedImage;
@@ -446,7 +448,7 @@ public class ViewHierarchy {
} else if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
return mDomNodeToView.get(((Attr) node).getOwnerElement());
} else if (node.getNodeType() == Node.DOCUMENT_NODE) {
- return mDomNodeToView.get(node.getOwnerDocument().getDocumentElement());
+ return mDomNodeToView.get(((Document) node).getDocumentElement());
}
}
@@ -600,7 +602,11 @@ public class ViewHierarchy {
* @return A {@link CanvasViewInfo} matching the given key, or null if not
* found.
*/
- public CanvasViewInfo findViewInfoFor(NodeProxy proxy) {
+ @Nullable
+ public CanvasViewInfo findViewInfoFor(@Nullable NodeProxy proxy) {
+ if (proxy == null) {
+ return null;
+ }
return mNodeToView.get(proxy.getNode());
}
@@ -710,6 +716,7 @@ public class ViewHierarchy {
/**
* Dumps a {@link ViewInfo} hierarchy to stdout
*
+ * @param session the corresponding session, if any
* @param info the {@link ViewInfo} object to dump
* @param depth the depth to indent it to
*/
@@ -736,6 +743,12 @@ public class ViewHierarchy {
sb.append("<"); //$NON-NLS-1$
sb.append(node.getDescriptor().getXmlName());
sb.append(">"); //$NON-NLS-1$
+
+ String id = node.getAttributeValue(ATTR_ID);
+ if (id != null && !id.isEmpty()) {
+ sb.append(" ");
+ sb.append(id);
+ }
} else if (cookie != null) {
sb.append(" " + cookie); //$NON-NLS-1$
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java
index 8337007..60f8365 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java
@@ -41,10 +41,12 @@ import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy;
import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
@@ -180,6 +182,18 @@ class ClientRulesEngine implements IClientRulesEngine {
}
@Override
+ @Nullable
+ public Object getViewObject(@NonNull INode node) {
+ ViewHierarchy views = mRulesEngine.getEditor().getCanvasControl().getViewHierarchy();
+ CanvasViewInfo vi = views.findViewInfoFor(node);
+ if (vi != null) {
+ return vi.getViewObject();
+ }
+
+ return null;
+ }
+
+ @Override
public @NonNull IViewMetadata getMetadata(final @NonNull String fqcn) {
return new IViewMetadata() {
@Override
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/GravityHelperTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/GravityHelperTest.java
index c05cdb5..f162924 100644
--- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/GravityHelperTest.java
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/GravityHelperTest.java
@@ -18,7 +18,12 @@ package com.android.ide.common.layout;
import static com.android.ide.common.layout.GravityHelper.GRAVITY_BOTTOM;
import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_HORIZ;
import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_VERT;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_FILL_HORIZ;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_FILL_VERT;
import static com.android.ide.common.layout.GravityHelper.GRAVITY_LEFT;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_RIGHT;
+import static com.android.ide.common.layout.GravityHelper.GRAVITY_TOP;
+import static com.android.ide.common.layout.GravityHelper.getGravity;
import junit.framework.TestCase;
@SuppressWarnings("javadoc")
@@ -29,4 +34,52 @@ public class GravityHelperTest extends TestCase {
assertEquals(GRAVITY_CENTER_HORIZ | GRAVITY_CENTER_VERT,
GravityHelper.getGravity("center", 0));
}
+
+ public void testGravityString() throws Exception {
+ assertEquals("left", getGravity(GRAVITY_LEFT));
+ assertEquals("right", getGravity(GRAVITY_RIGHT));
+ assertEquals("top", getGravity(GRAVITY_TOP));
+ assertEquals("bottom", getGravity(GRAVITY_BOTTOM));
+ assertEquals("center_horizontal", getGravity(GRAVITY_CENTER_HORIZ));
+ assertEquals("center_vertical", getGravity(GRAVITY_CENTER_VERT));
+ assertEquals("fill_horizontal", getGravity(GRAVITY_FILL_HORIZ));
+ assertEquals("fill_vertical", getGravity(GRAVITY_FILL_VERT));
+
+ assertEquals("center", getGravity(GRAVITY_CENTER_HORIZ|GRAVITY_CENTER_VERT));
+
+ assertEquals("left|bottom", getGravity(GRAVITY_LEFT|GRAVITY_BOTTOM));
+ assertEquals("center_horizontal|top", getGravity(GRAVITY_CENTER_HORIZ|GRAVITY_TOP));
+ }
+
+ public void testConstrained() throws Exception {
+ assertTrue(GravityHelper.isConstrainedHorizontally(GRAVITY_LEFT));
+ assertTrue(GravityHelper.isConstrainedHorizontally(GRAVITY_RIGHT));
+ assertTrue(GravityHelper.isConstrainedHorizontally(GRAVITY_CENTER_HORIZ));
+ assertTrue(GravityHelper.isConstrainedHorizontally(GRAVITY_FILL_HORIZ));
+
+ assertFalse(GravityHelper.isConstrainedVertically(GRAVITY_LEFT));
+ assertFalse(GravityHelper.isConstrainedVertically(GRAVITY_RIGHT));
+ assertFalse(GravityHelper.isConstrainedVertically(GRAVITY_CENTER_HORIZ));
+ assertFalse(GravityHelper.isConstrainedVertically(GRAVITY_FILL_HORIZ));
+
+ assertTrue(GravityHelper.isConstrainedVertically(GRAVITY_TOP));
+ assertTrue(GravityHelper.isConstrainedVertically(GRAVITY_BOTTOM));
+ assertTrue(GravityHelper.isConstrainedVertically(GRAVITY_CENTER_VERT));
+ assertTrue(GravityHelper.isConstrainedVertically(GRAVITY_FILL_VERT));
+
+ assertFalse(GravityHelper.isConstrainedHorizontally(GRAVITY_TOP));
+ assertFalse(GravityHelper.isConstrainedHorizontally(GRAVITY_BOTTOM));
+ assertFalse(GravityHelper.isConstrainedHorizontally(GRAVITY_CENTER_VERT));
+ assertFalse(GravityHelper.isConstrainedHorizontally(GRAVITY_FILL_VERT));
+ }
+
+ public void testAligned() throws Exception {
+ assertTrue(GravityHelper.isLeftAligned(GRAVITY_LEFT|GRAVITY_TOP));
+ assertTrue(GravityHelper.isLeftAligned(GRAVITY_LEFT));
+ assertFalse(GravityHelper.isLeftAligned(GRAVITY_RIGHT));
+
+ assertTrue(GravityHelper.isTopAligned(GRAVITY_LEFT|GRAVITY_TOP));
+ assertTrue(GravityHelper.isTopAligned(GRAVITY_TOP));
+ assertFalse(GravityHelper.isTopAligned(GRAVITY_BOTTOM));
+ }
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java
index 826f36c..b59144a 100644
--- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java
@@ -192,10 +192,10 @@ public class LayoutTestBase extends TestCase {
rule.onInitialize(fqn, new TestRulesEngine(fqn));
}
- private static class TestRulesEngine implements IClientRulesEngine {
+ public static class TestRulesEngine implements IClientRulesEngine {
private final String mFqn;
- protected TestRulesEngine(String fqn) {
+ public TestRulesEngine(String fqn) {
mFqn = fqn;
}
@@ -320,8 +320,14 @@ public class LayoutTestBase extends TestCase {
@Override
public int pxToDp(int px) {
- fail("Not supported in tests yet");
- return px;
+ // Arbitrary conversion
+ return px / 3;
+ }
+
+ @Override
+ public int dpToPx(int dp) {
+ // Arbitrary conversion
+ return 3 * dp;
}
@Override
@@ -333,17 +339,17 @@ public class LayoutTestBase extends TestCase {
@Override
public int screenToLayout(int pixels) {
fail("Not supported in tests yet");
- return 0;
+ return pixels;
}
@Override
- public int dpToPx(int dp) {
+ public @NonNull String getAppNameSpace() {
fail("Not supported in tests yet");
- return 0;
+ return null;
}
@Override
- public @NonNull String getAppNameSpace() {
+ public @Nullable Object getViewObject(@NonNull INode node) {
fail("Not supported in tests yet");
return null;
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java
index 8984f38..b9176f6 100644
--- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestNode.java
@@ -15,8 +15,13 @@
*/
package com.android.ide.common.layout;
-import static com.android.util.XmlUtils.ANDROID_URI;
+import static com.android.ide.common.layout.LayoutConstants.ANDROID_WIDGET_PREFIX;
import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
+import static com.android.tools.lint.detector.api.LintConstants.ANDROID_URI;
+import static junit.framework.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
@@ -25,14 +30,30 @@ import com.android.ide.common.api.INode;
import com.android.ide.common.api.INodeHandler;
import com.android.ide.common.api.Margins;
import com.android.ide.common.api.Rect;
+import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferences;
+import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle;
+import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.google.common.base.Splitter;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.io.IOException;
+import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import junit.framework.Assert;
/** Test/mock implementation of {@link INode} */
+@SuppressWarnings("javadoc")
public class TestNode implements INode {
private TestNode mParent;
@@ -193,8 +214,9 @@ public class TestNode implements INode {
@Override
public String toString() {
- return "TestNode [fqn=" + mFqcn + ", infos=" + mAttributeInfos
- + ", attributes=" + mAttributes + ", bounds=" + mBounds + "]";
+ String id = getStringAttr(ANDROID_URI, ATTR_ID);
+ return "TestNode [id=" + (id != null ? id : "?") + ", fqn=" + mFqcn + ", infos="
+ + mAttributeInfos + ", attributes=" + mAttributes + ", bounds=" + mBounds + "]";
}
@Override
@@ -215,4 +237,197 @@ public class TestNode implements INode {
public void setAttributeSources(List<String> attributeSources) {
mAttributeSources = attributeSources;
}
+
+ /** Create a test node from the given XML */
+ public static TestNode createFromXml(String xml) {
+ Document document = DomUtilities.parseDocument(xml, false);
+ assertNotNull(document);
+ assertNotNull(document.getDocumentElement());
+
+ return createFromNode(document.getDocumentElement());
+ }
+
+ public static String toXml(TestNode node) {
+ assertTrue("This method only works with nodes constructed from XML",
+ node instanceof TestXmlNode);
+ Document document = ((TestXmlNode) node).mElement.getOwnerDocument();
+ // Insert new whitespace nodes etc
+ String xml = dumpDocument(document);
+ document = DomUtilities.parseDocument(xml, false);
+
+ XmlPrettyPrinter printer = new XmlPrettyPrinter(XmlFormatPreferences.create(),
+ XmlFormatStyle.LAYOUT, "\n");
+ StringBuilder sb = new StringBuilder(1000);
+ sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
+ printer.prettyPrint(-1, document, null, null, sb, false);
+ return sb.toString();
+ }
+
+ @SuppressWarnings("deprecation")
+ private static String dumpDocument(Document document) {
+ // Diagnostics: print out the XML that we're about to render
+ org.apache.xml.serialize.OutputFormat outputFormat =
+ new org.apache.xml.serialize.OutputFormat(
+ "XML", "ISO-8859-1", true); //$NON-NLS-1$ //$NON-NLS-2$
+ outputFormat.setIndent(2);
+ outputFormat.setLineWidth(100);
+ outputFormat.setIndenting(true);
+ outputFormat.setOmitXMLDeclaration(true);
+ outputFormat.setOmitDocumentType(true);
+ StringWriter stringWriter = new StringWriter();
+ // Using FQN here to avoid having an import above, which will result
+ // in a deprecation warning, and there isn't a way to annotate a single
+ // import element with a SuppressWarnings.
+ org.apache.xml.serialize.XMLSerializer serializer =
+ new org.apache.xml.serialize.XMLSerializer(stringWriter, outputFormat);
+ serializer.setNamespaces(true);
+ try {
+ serializer.serialize(document.getDocumentElement());
+ return stringWriter.toString();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ private static TestNode createFromNode(Element element) {
+ String fqcn = ANDROID_WIDGET_PREFIX + element.getTagName();
+ TestNode node = new TestXmlNode(fqcn, element);
+
+ for (Element child : DomUtilities.getChildren(element)) {
+ node.add(createFromNode(child));
+ }
+
+ return node;
+ }
+
+ @Nullable
+ public static TestNode findById(TestNode node, String id) {
+ id = BaseLayoutRule.stripIdPrefix(id);
+ return node.findById(id);
+ }
+
+ private TestNode findById(String targetId) {
+ String id = getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id != null && targetId.equals(BaseLayoutRule.stripIdPrefix(id))) {
+ return this;
+ }
+
+ for (TestNode child : mChildren) {
+ TestNode result = child.findById(targetId);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ private static String getTagName(String fqcn) {
+ return fqcn.substring(fqcn.lastIndexOf('.') + 1);
+ }
+
+ private static class TestXmlNode extends TestNode {
+ private final Element mElement;
+
+ public TestXmlNode(String fqcn, Element element) {
+ super(fqcn);
+ mElement = element;
+ }
+
+ @Override
+ public boolean setAttribute(String uri, String localName, String value) {
+ mElement.setAttributeNS(uri, localName, value);
+ return super.setAttribute(uri, localName, value);
+ }
+
+ @Override
+ public INode appendChild(String viewFqcn) {
+ Element child = mElement.getOwnerDocument().createElement(getTagName(viewFqcn));
+ mElement.appendChild(child);
+ return new TestXmlNode(viewFqcn, child);
+ }
+
+ @Override
+ public INode insertChildAt(String viewFqcn, int index) {
+ if (index == -1) {
+ return appendChild(viewFqcn);
+ }
+ Element child = mElement.getOwnerDocument().createElement(getTagName(viewFqcn));
+ List<Element> children = DomUtilities.getChildren(mElement);
+ if (children.size() >= index) {
+ Element before = children.get(index);
+ mElement.insertBefore(child, before);
+ } else {
+ fail("Unexpected index");
+ mElement.appendChild(child);
+ }
+ return new TestXmlNode(viewFqcn, child);
+ }
+
+ @Override
+ public String getStringAttr(String uri, String name) {
+ String value;
+ if (uri == null) {
+ value = mElement.getAttribute(name);
+ } else {
+ value = mElement.getAttributeNS(uri, name);
+ }
+ if (value.isEmpty()) {
+ value = null;
+ }
+
+ return value;
+ }
+
+ @Override
+ public void removeChild(INode node) {
+ assert node instanceof TestXmlNode;
+ mElement.removeChild(((TestXmlNode) node).mElement);
+ }
+
+ @Override
+ public void removeChild(int index) {
+ List<Element> children = DomUtilities.getChildren(mElement);
+ assertTrue(index < children.size());
+ Element oldChild = children.get(index);
+ mElement.removeChild(oldChild);
+ }
+ }
+
+ // Recursively initialize this node with the bounds specified in the given hierarchy
+ // dump (from ViewHierarchy's DUMP_INFO flag
+ public void assignBounds(String bounds) {
+ Iterable<String> split = Splitter.on('\n').trimResults().split(bounds);
+ assignBounds(split.iterator());
+ }
+
+ private void assignBounds(Iterator<String> iterator) {
+ assertTrue(iterator.hasNext());
+ String desc = iterator.next();
+
+ Pattern pattern = Pattern.compile("^\\s*(.+)\\s+\\[(.+)\\]\\s*(<.+>)?\\s*(\\S+)?\\s*$");
+ Matcher matcher = pattern.matcher(desc);
+ assertTrue(matcher.matches());
+ String fqn = matcher.group(1);
+ assertEquals(getFqcn(), fqn);
+ String boundsString = matcher.group(2);
+ String[] bounds = boundsString.split(",");
+ assertEquals(boundsString, 4, bounds.length);
+ try {
+ int left = Integer.parseInt(bounds[0]);
+ int top = Integer.parseInt(bounds[1]);
+ int right = Integer.parseInt(bounds[2]);
+ int bottom = Integer.parseInt(bounds[3]);
+ mBounds = new Rect(left, top, right - left, bottom - top);
+ } catch (NumberFormatException nufe) {
+ Assert.fail(nufe.getLocalizedMessage());
+ }
+ String tag = matcher.group(3);
+
+ for (INode child : getChildren()) {
+ assertTrue(iterator.hasNext());
+ ((TestNode) child).assignBounds(iterator);
+ }
+ }
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/grid/GridModelTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/grid/GridModelTest.java
index 680b7ca..f3405c1 100644
--- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/grid/GridModelTest.java
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/grid/GridModelTest.java
@@ -15,15 +15,24 @@
*/
package com.android.ide.common.layout.grid;
-import static com.android.util.XmlUtils.ANDROID_URI;
import static com.android.ide.common.layout.LayoutConstants.ATTR_COLUMN_COUNT;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN_SPAN;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW;
import static com.android.ide.common.layout.LayoutConstants.FQCN_BUTTON;
+import static com.android.util.XmlUtils.ANDROID_URI;
+import com.android.ide.common.api.INode;
import com.android.ide.common.api.Rect;
import com.android.ide.common.layout.LayoutTestBase;
import com.android.ide.common.layout.TestNode;
+import com.android.ide.common.layout.grid.GridModel.ViewData;
+import java.util.Arrays;
+import java.util.Collections;
+
+@SuppressWarnings("javadoc")
public class GridModelTest extends LayoutTestBase {
public void testRemoveFlag() {
assertEquals("left", GridModel.removeFlag("top", "top|left"));
@@ -38,7 +47,7 @@ public class GridModelTest extends LayoutTestBase {
TestNode targetNode = TestNode.create("android.widget.GridLayout").id("@+id/GridLayout1")
.bounds(new Rect(0, 0, 240, 480)).set(ANDROID_URI, ATTR_COLUMN_COUNT, "3");
- GridModel model = new GridModel(null, targetNode, null);
+ GridModel model = GridModel.get(null, targetNode, null);
assertEquals(3, model.declaredColumnCount);
assertEquals(1, model.actualColumnCount);
assertEquals(1, model.actualRowCount);
@@ -48,9 +57,796 @@ public class GridModelTest extends LayoutTestBase {
targetNode.add(TestNode.create(FQCN_BUTTON).id("@+id/Button3"));
targetNode.add(TestNode.create(FQCN_BUTTON).id("@+id/Button4"));
- model = new GridModel(null, targetNode, null);
+ model = GridModel.get(null, targetNode, null);
+ assertEquals(3, model.declaredColumnCount);
+ assertEquals(3, model.actualColumnCount);
+ assertEquals(2, model.actualRowCount);
+ }
+
+ public void testSplitColumn() {
+ TestNode targetNode = TestNode.create("android.widget.GridLayout").id("@+id/GridLayout1")
+ .bounds(new Rect(0, 0, 240, 480)).set(ANDROID_URI, ATTR_COLUMN_COUNT, "3");
+ TestNode b1 = TestNode.create(FQCN_BUTTON).id("@+id/Button1");
+ TestNode b2 = TestNode.create(FQCN_BUTTON).id("@+id/Button2");
+ TestNode b3 = TestNode.create(FQCN_BUTTON).id("@+id/Button3");
+ TestNode b4 = TestNode.create(FQCN_BUTTON).id("@+id/Button4");
+ targetNode.add(b1);
+ targetNode.add(b2);
+ targetNode.add(b3);
+ targetNode.add(b4);
+ b4.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN_SPAN, "2");
+
+ GridModel model = GridModel.get(new LayoutTestBase.TestRulesEngine(targetNode.getFqcn()),
+ targetNode, null);
+ assertEquals(3, model.declaredColumnCount);
+ assertEquals(3, model.actualColumnCount);
+ assertEquals(2, model.actualRowCount);
+
+ model.applyPositionAttributes();
+ assertEquals("0", b1.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN));
+ assertEquals("0", b1.getStringAttr(ANDROID_URI, ATTR_LAYOUT_ROW));
+
+ assertEquals("1", b2.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN));
+ assertEquals("0", b2.getStringAttr(ANDROID_URI, ATTR_LAYOUT_ROW));
+
+ assertEquals("2", b3.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN));
+ assertEquals("0", b3.getStringAttr(ANDROID_URI, ATTR_LAYOUT_ROW));
+
+ assertEquals("0", b4.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN));
+ assertEquals("1", b4.getStringAttr(ANDROID_URI, ATTR_LAYOUT_ROW));
+ assertEquals("2", b4.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN_SPAN));
+
+ model.splitColumn(1, false /*insertMarginColumn*/, 100 /*columnWidthDp*/, 300 /* x */);
+ model.applyPositionAttributes();
+
+ assertEquals(4, model.declaredColumnCount);
+ assertEquals(4, model.actualColumnCount);
+ assertEquals(2, model.actualRowCount);
+
+ assertEquals("0", b1.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN));
+ assertEquals("0", b1.getStringAttr(ANDROID_URI, ATTR_LAYOUT_ROW));
+
+ assertEquals("1", b2.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN));
+ assertEquals("2", b2.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN_SPAN));
+ assertEquals("0", b2.getStringAttr(ANDROID_URI, ATTR_LAYOUT_ROW));
+
+ assertEquals("3", b3.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN));
+ assertEquals("0", b3.getStringAttr(ANDROID_URI, ATTR_LAYOUT_ROW));
+
+ assertEquals("0", b4.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN));
+ assertEquals("1", b4.getStringAttr(ANDROID_URI, ATTR_LAYOUT_ROW));
+ assertEquals("3", b4.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN_SPAN));
+ }
+
+ public void testDeletion1() {
+ String xml =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "<GridLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
+ " android:layout_width=\"match_parent\"\n" +
+ " android:layout_height=\"match_parent\"\n" +
+ " android:columnCount=\"4\" >\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button1\"\n" +
+ " android:layout_column=\"1\"\n" +
+ " android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"1\"\n" +
+ " android:text=\"Button\" />\n" +
+ "\n" +
+ " <TextView\n" +
+ " android:id=\"@+id/TextView1\"\n" +
+ " android:layout_column=\"3\"\n" +
+ " android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"1\"\n" +
+ " android:text=\"Text\" />\n" +
+ "\n" +
+ " <Space\n" +
+ " android:id=\"@+id/wspace1\"\n" +
+ " android:layout_width=\"21dp\"\n" +
+ " android:layout_height=\"1dp\"\n" +
+ " android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ "\n" +
+ " <Space\n" +
+ " android:id=\"@+id/hspace1\"\n" +
+ " android:layout_width=\"1dp\"\n" +
+ " android:layout_height=\"55dp\"\n" +
+ " android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ "\n" +
+ " <Space\n" +
+ " android:id=\"@+id/wspace2\"\n" +
+ " android:layout_width=\"10dp\"\n" +
+ " android:layout_height=\"1dp\"\n" +
+ " android:layout_column=\"2\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ "\n" +
+ "</GridLayout>";
+
+ TestNode targetNode = TestNode.createFromXml(xml);
+ assertNotNull(targetNode);
+
+ TestNode button1 = TestNode.findById(targetNode, "@+id/button1");
+ TestNode textView1 = TestNode.findById(targetNode, "@+id/TextView1");
+ TestNode wspace1 = TestNode.findById(targetNode, "@+id/wspace1");
+ TestNode wspace2 = TestNode.findById(targetNode, "@+id/wspace2");
+ TestNode hspace1 = TestNode.findById(targetNode, "@+id/hspace1");
+ assertNotNull(wspace1);
+ assertNotNull(hspace1);
+ assertNotNull(wspace2);
+ assertNotNull(button1);
+ assertNotNull(textView1);
+
+ // Assign some bounds such that the model makes sense when merging spacer sizes
+ // TODO: MAke test utility method to automatically assign half divisions!!
+ button1.bounds(new Rect(90, 10, 100, 40));
+ textView1.bounds(new Rect(200, 10, 100, 40));
+ wspace1.bounds(new Rect(0, 0, 90, 1));
+ wspace1.bounds(new Rect(190, 0, 10, 1));
+ hspace1.bounds(new Rect(0, 0, 1, 10));
+
+ GridModel model = GridModel.get(new LayoutTestBase.TestRulesEngine(targetNode.getFqcn()),
+ targetNode, null);
+ assertEquals(4, model.declaredColumnCount);
+ assertEquals(4, model.actualColumnCount);
+ assertEquals(2, model.actualRowCount);
+
+ ViewData textViewData = model.getView(textView1);
+ assertEquals(3, textViewData.column);
+
+ // Delete button1
+ button1.getParent().removeChild(button1);
+ model.onDeleted(Arrays.<INode>asList(button1));
+ model.applyPositionAttributes();
+
+ assertEquals(2, model.declaredColumnCount);
+ assertEquals(2, model.actualColumnCount);
+ assertEquals(2, model.actualRowCount);
+ assertNotNull(model.getView(textView1));
+ assertNull(model.getView(button1));
+
+ assertEquals(
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "<GridLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
+ " android:layout_width=\"match_parent\"\n" +
+ " android:layout_height=\"match_parent\"\n" +
+ " android:columnCount=\"2\">\n" +
+ "\n" +
+ " <TextView\n" +
+ " android:id=\"@+id/TextView1\"\n" +
+ " android:layout_column=\"1\"\n" +
+ " android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"1\"\n" +
+ " android:text=\"Text\">\n" +
+ " </TextView>\n" +
+ "\n" +
+ " <Space\n" +
+ " android:id=\"@+id/wspace1\"\n" +
+ " android:layout_width=\"66dp\"\n" +
+ " android:layout_height=\"1dp\"\n" +
+ " android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\">\n" +
+ " </Space>\n" +
+ "\n" +
+ " <Space\n" +
+ " android:id=\"@+id/hspace1\"\n" +
+ " android:layout_width=\"1dp\"\n" +
+ " android:layout_height=\"55dp\"\n" +
+ " android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\">\n" +
+ " </Space>\n" +
+ "\n" +
+ "</GridLayout>", TestNode.toXml(targetNode));
+
+ // Delete textView1
+
+ textView1.getParent().removeChild(textView1);
+ model.onDeleted(Arrays.<INode>asList(textView1));
+ model.applyPositionAttributes();
+
+ assertEquals(2, model.declaredColumnCount);
+ assertEquals(0, model.actualColumnCount);
+ assertEquals(0, model.actualRowCount);
+ assertNull(model.getView(textView1));
+ assertNull(model.getView(button1));
+
+ assertEquals(
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "<GridLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
+ " android:layout_width=\"match_parent\"\n" +
+ " android:layout_height=\"match_parent\"\n" +
+ " android:columnCount=\"0\">\n" +
+ "\n" +
+ "</GridLayout>", TestNode.toXml(targetNode));
+
+ }
+
+ public void testDelete2() throws Exception {
+ String xml =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "<GridLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
+ " android:layout_width=\"match_parent\"\n" +
+ " android:layout_height=\"match_parent\"\n" +
+ " android:columnCount=\"4\"\n" +
+ " android:orientation=\"vertical\" >\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button1\"\n" +
+ " android:layout_column=\"0\"\n" +
+ " android:layout_columnSpan=\"2\"\n" +
+ " android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"0\"\n" +
+ " android:text=\"Button1\" />\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button3\"\n" +
+ " android:layout_column=\"1\"\n" +
+ " android:layout_columnSpan=\"2\"\n" +
+ " android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"1\"\n" +
+ " android:text=\"Button2\" />\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button2\"\n" +
+ " android:layout_column=\"2\"\n" +
+ " android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"0\"\n" +
+ " android:text=\"Button3\" />\n" +
+ "\n" +
+ " <Space\n" +
+ " android:id=\"@+id/spacer_177\"\n" +
+ " android:layout_width=\"46dp\"\n" +
+ " android:layout_height=\"1dp\"\n" +
+ " android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ "\n" +
+ "</GridLayout>";
+
+ TestNode targetNode = TestNode.createFromXml(xml);
+ assertNotNull(targetNode);
+
+ TestNode button1 = TestNode.findById(targetNode, "@+id/button1");
+ TestNode button2 = TestNode.findById(targetNode, "@+id/button2");
+ TestNode button3 = TestNode.findById(targetNode, "@+id/button3");
+ TestNode hspacer = TestNode.findById(targetNode, "@+id/spacer_177");
+ assertNotNull(button1);
+ assertNotNull(button2);
+ assertNotNull(button3);
+ assertNotNull(hspacer);
+
+ // Assign some bounds such that the model makes sense when merging spacer sizes
+ // TODO: MAke test utility method to automatically assign half divisions!!
+ button1.bounds(new Rect(0, 0, 100, 40));
+ button2.bounds(new Rect(100, 0, 100, 40));
+ button3.bounds(new Rect(50, 40, 100, 40));
+ hspacer.bounds(new Rect(0, 0, 50, 1));
+
+ GridModel model = GridModel.get(new LayoutTestBase.TestRulesEngine(targetNode.getFqcn()),
+ targetNode, null);
+ assertEquals(4, model.declaredColumnCount);
+ assertEquals(3, model.actualColumnCount);
+ assertEquals(2, model.actualRowCount);
+
+ ViewData buttonData = model.getView(button1);
+ assertEquals(0, buttonData.column);
+
+ // Delete button1
+ button1.getParent().removeChild(button1);
+ model.onDeleted(Arrays.<INode>asList(button1));
+ model.applyPositionAttributes();
+
+ assertEquals(3, model.declaredColumnCount);
+ assertEquals(3, model.actualColumnCount);
+ assertEquals(2, model.actualRowCount);
+ assertNull(model.getView(button1));
+
+ assertEquals(
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "<GridLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
+ " android:layout_width=\"match_parent\"\n" +
+ " android:layout_height=\"match_parent\"\n" +
+ " android:columnCount=\"3\"\n" +
+ " android:orientation=\"vertical\">\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button3\"\n" +
+ " android:layout_column=\"1\"\n" +
+ " android:layout_columnSpan=\"2\"\n" +
+ " android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"1\"\n" +
+ " android:text=\"Button2\">\n" +
+ " </Button>\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button2\"\n" +
+ " android:layout_column=\"2\"\n" +
+ " android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"0\"\n" +
+ " android:text=\"Button3\">\n" +
+ " </Button>\n" +
+ "\n" +
+ " <Space\n" +
+ " android:id=\"@+id/spacer_177\"\n" +
+ " android:layout_width=\"46dp\"\n" +
+ " android:layout_height=\"1dp\"\n" +
+ " android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\">\n" +
+ " </Space>\n" +
+ "\n" +
+ "</GridLayout>", TestNode.toXml(targetNode));
+ }
+
+ public void testDelete3_INCOMPLETE() throws Exception {
+ String xml =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "<GridLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
+ " android:layout_width=\"match_parent\" android:layout_height=\"match_parent\"\n" +
+ " android:columnCount=\"6\">\n" +
+ " <Button android:id=\"@+id/button1\" android:layout_column=\"1\"\n" +
+ " android:layout_columnSpan=\"2\" android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"1\" android:layout_rowSpan=\"2\" android:text=\"Button\" />\n" +
+ " <TextView android:id=\"@+id/TextView1\" android:layout_column=\"4\"\n" +
+ " android:layout_gravity=\"left|top\" android:layout_row=\"1\"\n" +
+ " android:text=\"Text\" />\n" +
+ " <Button android:id=\"@+id/button3\" android:layout_column=\"5\"\n" +
+ " android:layout_gravity=\"left|top\" android:layout_row=\"2\"\n" +
+ " android:layout_rowSpan=\"2\" android:text=\"Button\" />\n" +
+ " <Button android:id=\"@+id/button2\" android:layout_column=\"2\"\n" +
+ " android:layout_columnSpan=\"3\" android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"4\" android:text=\"Button\" />\n" +
+ " <Space android:id=\"@+id/wspace1\" android:layout_width=\"21dp\"\n" +
+ " android:layout_height=\"1dp\" android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ " <Space android:id=\"@+id/spacer_630\" android:layout_width=\"1dp\"\n" +
+ " android:layout_height=\"55dp\" android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ " <Space android:id=\"@+id/wspace2\" android:layout_width=\"10dp\"\n" +
+ " android:layout_height=\"1dp\" android:layout_column=\"3\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ " <Space android:id=\"@+id/spacer_619\" android:layout_width=\"59dp\"\n" +
+ " android:layout_height=\"1dp\" android:layout_column=\"1\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ " <Space android:id=\"@+id/spacer_102\" android:layout_width=\"1dp\"\n" +
+ " android:layout_height=\"30dp\" android:layout_column=\"0\"\n" +
+ " android:layout_row=\"1\" />\n" +
+ " <Space android:id=\"@+id/spacer_109\" android:layout_width=\"1dp\"\n" +
+ " android:layout_height=\"28dp\" android:layout_column=\"0\"\n" +
+ " android:layout_row=\"2\" />\n" +
+ " <Space android:id=\"@+id/spacer_146\" android:layout_width=\"1dp\"\n" +
+ " android:layout_height=\"70dp\" android:layout_column=\"0\"\n" +
+ " android:layout_row=\"3\" />\n" +
+ "</GridLayout>";
+ TestNode targetNode = TestNode.createFromXml(xml);
+ assertNotNull(targetNode);
+ targetNode.assignBounds(
+ "android.widget.GridLayout [0,109,480,800] <GridLayout>\n" +
+ " android.widget.Button [32,83,148,155] <Button> @+id/button1\n" +
+ " android.widget.TextView [163,83,205,109] <TextView> @+id/TextView1\n" +
+ " android.widget.Button [237,128,353,200] <Button> @+id/button3\n" +
+ " android.widget.Button [121,275,237,347] <Button> @+id/button2\n" +
+ " android.widget.Space [0,0,32,2] <Space> @+id/wspace1\n" +
+ " android.widget.Space [0,0,2,83] <Space> @+id/spacer_630\n" +
+ " android.widget.Space [148,0,163,2] <Space> @+id/wspace2\n" +
+ " android.widget.Space [32,0,121,2] <Space> @+id/spacer_619\n" +
+ " android.widget.Space [0,83,2,128] <Space> @+id/spacer_102\n" +
+ " android.widget.Space [0,128,2,170] <Space> @+id/spacer_109\n" +
+ " android.widget.Space [0,170,2,275] <Space> @+id/spacer_146\n");
+ TestNode layout = TestNode.findById(targetNode, "@+id/GridLayout1");
+ //TestNode button1 = TestNode.findById(targetNode, "@+id/button1");
+ TestNode button2 = TestNode.findById(targetNode, "@+id/button2");
+ TestNode button3 = TestNode.findById(targetNode, "@+id/button3");
+
+ GridModel model = GridModel.get(new LayoutTestBase.TestRulesEngine(targetNode.getFqcn()),
+ targetNode, null);
+ assertEquals(6, model.declaredColumnCount);
+ assertEquals(6, model.actualColumnCount);
+ assertEquals(5, model.actualRowCount);
+
+ // TODO: Delete button2 or button3: bad stuff happens visually
+ fail("Finish test");
+ }
+
+ public void testDelete4_INCOMPLETE() {
+ String xml = "" +
+ "<GridLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
+ " xmlns:tools=\"http://schemas.android.com/tools\" " +
+ " android:layout_width=\"match_parent\"\n" +
+ " android:layout_height=\"match_parent\" android:columnCount=\"3\"\n" +
+ " android:gravity=\"center\" android:text=\"@string/hello_world\"\n" +
+ " tools:context=\".MainActivity\">\n" +
+ " <Button android:id=\"@+id/button2\" android:layout_column=\"1\"\n" +
+ " android:layout_columnSpan=\"2\" android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"1\" android:text=\"Button\" />\n" +
+ " <Button android:id=\"@+id/button1\" android:layout_column=\"1\"\n" +
+ " android:layout_columnSpan=\"2\" android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"3\" android:text=\"Button\" />\n" +
+ " <Space android:id=\"@+id/spacer_167\" android:layout_width=\"74dp\"\n" +
+ " android:layout_height=\"1dp\" android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ " <Space android:id=\"@+id/spacer_133\" android:layout_width=\"1dp\"\n" +
+ " android:layout_height=\"21dp\" android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ " <Space android:id=\"@+id/spacer_142\" android:layout_width=\"1dp\"\n" +
+ " android:layout_height=\"26dp\" android:layout_column=\"0\"\n" +
+ " android:layout_row=\"2\" />\n" +
+ " <Space android:id=\"@+id/spacer_673\" android:layout_width=\"43dp\"\n" +
+ " android:layout_height=\"1dp\" android:layout_column=\"1\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ " <Space android:id=\"@+id/spacer_110\" android:layout_width=\"202dp\"\n" +
+ " android:layout_height=\"15dp\" android:layout_column=\"2\" />\n" +
+ "</GridLayout>";
+ TestNode targetNode = TestNode.createFromXml(xml);
+ assertNotNull(targetNode);
+ targetNode.assignBounds(
+ "android.widget.GridLayout [0,109,480,800] <GridLayout>\n" +
+ " android.widget.Button [111,32,227,104] <Button> @+id/button2\n" +
+ " android.widget.Button [111,143,227,215] <Button> @+id/button1\n" +
+ " android.widget.Space [0,0,111,2] <Space> @+id/spacer_167\n" +
+ " android.widget.Space [0,0,2,32] <Space> @+id/spacer_133\n" +
+ " android.widget.Space [0,104,2,143] <Space> @+id/spacer_142\n" +
+ " android.widget.Space [111,0,176,2] <Space> @+id/spacer_673\n" +
+ " android.widget.Space [176,668,479,691] <Space> @+id/spacer_110");
+
+
+ // Remove button2; button1 shifts to the right!
+
+ //TestNode layout = TestNode.findById(targetNode, "@+id/GridLayout1");
+ //TestNode button1 = TestNode.findById(targetNode, "@+id/button1");
+ TestNode button2 = TestNode.findById(targetNode, "@+id/button2");
+ TestNode button3 = TestNode.findById(targetNode, "@+id/button3");
+ assertEquals(new Rect(111, 32, 227 - 111, 104 - 32), button2.getBounds());
+
+ GridModel model = GridModel.get(new LayoutTestBase.TestRulesEngine(targetNode.getFqcn()),
+ targetNode, null);
+ assertEquals(3, model.declaredColumnCount);
+ assertEquals(3, model.actualColumnCount);
+ assertEquals(4, model.actualRowCount);
+ fail("Finish test");
+ }
+
+ public void testDelete5_INCOMPLETE() {
+ String xml =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "<GridLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
+ " android:id=\"@+id/GridLayout1\" android:layout_width=\"match_parent\"\n" +
+ " android:layout_height=\"match_parent\" android:columnCount=\"4\"\n" +
+ " android:orientation=\"vertical\">\n" +
+ " <Button android:id=\"@+id/button1\" android:layout_column=\"0\"\n" +
+ " android:layout_gravity=\"center_horizontal|bottom\"\n" +
+ " android:layout_row=\"0\" android:text=\"Button\" />\n" +
+ " <Space android:layout_width=\"66dp\" android:layout_height=\"1dp\"\n" +
+ " android:layout_column=\"0\" android:layout_row=\"0\" />\n" +
+ " <Button android:id=\"@+id/button3\" android:layout_column=\"2\"\n" +
+ " android:layout_gravity=\"left|bottom\" android:layout_row=\"0\"\n" +
+ " android:text=\"Button\" />\n" +
+ " <Button android:id=\"@+id/button2\" android:layout_column=\"3\"\n" +
+ " android:layout_columnSpan=\"2\" android:layout_gravity=\"left|bottom\"\n" +
+ " android:layout_row=\"0\" android:text=\"Button\" />\n" +
+ " <Space android:id=\"@+id/spacer_109\" android:layout_width=\"51dp\"\n" +
+ " android:layout_height=\"1dp\" android:layout_column=\"1\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ " <Space android:layout_width=\"129dp\" android:layout_height=\"1dp\"\n" +
+ " android:layout_column=\"2\" android:layout_row=\"0\" />\n" +
+ " <Space android:layout_width=\"62dp\" android:layout_height=\"1dp\"\n" +
+ " android:layout_column=\"3\" android:layout_row=\"0\" />\n" +
+ " <Space android:id=\"@+id/spacer_397\" android:layout_width=\"1dp\"\n" +
+ " android:layout_height=\"103dp\" android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ " <Space android:layout_width=\"1dp\" android:layout_height=\"356dp\"\n" +
+ " android:layout_column=\"0\" android:layout_row=\"1\" />\n" +
+ "</GridLayout>";
+ TestNode targetNode = TestNode.createFromXml(xml);
+ assertNotNull(targetNode);
+
+ targetNode.assignBounds(
+ "android.widget.GridLayout [0,109,480,800] <GridLayout> @+id/GridLayout1\n" +
+ " android.widget.Button [0,83,116,155] <Button> @+id/button1\n" +
+ " android.widget.Space [0,0,99,2] <Space>\n" +
+ " android.widget.Button [193,83,309,155] <Button> @+id/button3\n" +
+ " android.widget.Button [387,83,503,155] <Button> @+id/button2\n" +
+ " android.widget.Space [116,0,193,2] <Space> @+id/spacer_109\n" +
+ " android.widget.Space [193,0,387,2] <Space>\n" +
+ " android.widget.Space [387,0,480,2] <Space>\n" +
+ " android.widget.Space [0,0,2,155] <Space> @+id/spacer_397\n" +
+ " android.widget.Space [0,155,2,689] <Space>");
+
+ // Delete button3. This causes an array out of bounds exception currently.
+
+ //TestNode layout = TestNode.findById(targetNode, "@+id/GridLayout1");
+ //TestNode button1 = TestNode.findById(targetNode, "@+id/button1");
+ TestNode button2 = TestNode.findById(targetNode, "@+id/button2");
+ TestNode button3 = TestNode.findById(targetNode, "@+id/button3");
+ assertEquals(new Rect(387, 83, 503 - 387, 155- 83), button2.getBounds());
+
+ GridModel model = GridModel.get(new LayoutTestBase.TestRulesEngine(targetNode.getFqcn()),
+ targetNode, null);
+ assertEquals(4, model.declaredColumnCount);
+ assertEquals(4, model.actualColumnCount);
+ assertEquals(2, model.actualRowCount);
+
+ model.onDeleted(Collections.<INode>singletonList(button3));
+ // Exception fixed; todo: Test that the model updates are correct.
+
assertEquals(3, model.declaredColumnCount);
assertEquals(3, model.actualColumnCount);
assertEquals(2, model.actualRowCount);
+
+ fail("Finish test");
+ }
+
+ public void testInsert1() throws Exception {
+ String xml =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "<GridLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
+ " android:id=\"@+id/GridLayout1\"\n" +
+ " android:layout_width=\"match_parent\"\n" +
+ " android:layout_height=\"match_parent\"\n" +
+ " android:columnCount=\"4\"\n" +
+ " android:orientation=\"vertical\" >\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button1\"\n" +
+ " android:layout_column=\"0\"\n" +
+ " android:layout_columnSpan=\"4\"\n" +
+ " android:layout_gravity=\"center_horizontal|bottom\"\n" +
+ " android:layout_row=\"0\"\n" +
+ " android:text=\"Button\" />\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button2\"\n" +
+ " android:layout_column=\"2\"\n" +
+ " android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"1\"\n" +
+ " android:text=\"Button\" />\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button3\"\n" +
+ " android:layout_column=\"3\"\n" +
+ " android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"1\"\n" +
+ " android:text=\"Button\" />\n" +
+ "\n" +
+ " <Space\n" +
+ " android:id=\"@+id/spacer_393\"\n" +
+ " android:layout_width=\"81dp\"\n" +
+ " android:layout_height=\"1dp\"\n" +
+ " android:layout_column=\"1\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ "\n" +
+ " <Space\n" +
+ " android:id=\"@+id/spacer_397\"\n" +
+ " android:layout_width=\"1dp\"\n" +
+ " android:layout_height=\"103dp\"\n" +
+ " android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ "\n" +
+ "</GridLayout>";
+
+ TestNode targetNode = TestNode.createFromXml(xml);
+ assertNotNull(targetNode);
+
+ TestNode layout = TestNode.findById(targetNode, "@+id/GridLayout1");
+ TestNode button1 = TestNode.findById(targetNode, "@+id/button1");
+ TestNode button2 = TestNode.findById(targetNode, "@+id/button2");
+ TestNode button3 = TestNode.findById(targetNode, "@+id/button3");
+ TestNode hspacer = TestNode.findById(targetNode, "@+id/spacer_393");
+ TestNode vspacer = TestNode.findById(targetNode, "@+id/spacer_397");
+ assertNotNull(layout);
+ assertNotNull(button1);
+ assertNotNull(button2);
+ assertNotNull(button3);
+ assertNotNull(hspacer);
+
+ // Obtained by setting ViewHierarchy.DUMP_INFO=true:
+ layout.bounds(new Rect(0, 109, 480, 800-109));
+ button1.bounds(new Rect(182, 83, 298-182, 155-83));
+ button2.bounds(new Rect(124, 155, 240-124, 227-155));
+ button3.bounds(new Rect(240, 155, 356-240, 227-155));
+ hspacer.bounds(new Rect(2, 0, 124-2, 2));
+ vspacer.bounds(new Rect(0, 0, 2, 155));
+
+ GridModel model = GridModel.get(new LayoutTestBase.TestRulesEngine(targetNode.getFqcn()),
+ targetNode, null);
+ assertEquals(4, model.declaredColumnCount);
+ assertEquals(4, model.actualColumnCount);
+ assertEquals(2, model.actualRowCount);
+
+
+ model.splitColumn(1, false, 21, 32);
+ int index = model.getInsertIndex(2, 1);
+ GridModel.ViewData next = model.getView(index);
+ INode newChild = targetNode.insertChildAt(FQCN_BUTTON, index);
+ next.applyPositionAttributes();
+ model.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, 1);
+ model.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN_SPAN, 3);
+ }
+
+ public void testInsert2() throws Exception {
+ // Drop into a view where there is a centered view: when dropping to the right of
+ // it (on a row further down), ensure that the row span is increased for the
+ // non-left-justified centered view which does not horizontally overlap the view
+ String xml =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "<GridLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
+ " android:id=\"@+id/GridLayout1\"\n" +
+ " android:layout_width=\"match_parent\"\n" +
+ " android:layout_height=\"match_parent\"\n" +
+ " android:columnCount=\"3\"\n" +
+ " android:orientation=\"vertical\" >\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button1\"\n" +
+ " android:layout_column=\"0\"\n" +
+ " android:layout_columnSpan=\"3\"\n" +
+ " android:layout_gravity=\"center_horizontal|bottom\"\n" +
+ " android:layout_row=\"0\"\n" +
+ " android:text=\"Button\" />\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button2\"\n" +
+ " android:layout_column=\"1\"\n" +
+ " android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"1\"\n" +
+ " android:text=\"Button\" />\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button3\"\n" +
+ " android:layout_column=\"2\"\n" +
+ " android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"1\"\n" +
+ " android:text=\"Button\" />\n" +
+ "\n" +
+ " <Space\n" +
+ " android:id=\"@+id/spacer_393\"\n" +
+ " android:layout_width=\"81dp\"\n" +
+ " android:layout_height=\"1dp\"\n" +
+ " android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ "\n" +
+ " \n" +
+ " <Space\n" +
+ " android:id=\"@+id/spacer_397\"\n" +
+ " android:layout_width=\"1dp\"\n" +
+ " android:layout_height=\"103dp\"\n" +
+ " android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ "\n" +
+ " \n" +
+ "</GridLayout>";
+
+ TestNode targetNode = TestNode.createFromXml(xml);
+ assertNotNull(targetNode);
+
+ TestNode layout = TestNode.findById(targetNode, "@+id/GridLayout1");
+ TestNode button1 = TestNode.findById(targetNode, "@+id/button1");
+ TestNode button2 = TestNode.findById(targetNode, "@+id/button2");
+ TestNode button3 = TestNode.findById(targetNode, "@+id/button3");
+ TestNode hspacer = TestNode.findById(targetNode, "@+id/spacer_393");
+ TestNode vspacer = TestNode.findById(targetNode, "@+id/spacer_397");
+ assertNotNull(layout);
+ assertNotNull(button1);
+ assertNotNull(button2);
+ assertNotNull(button3);
+ assertNotNull(hspacer);
+
+ // Obtained by setting ViewHierarchy.DUMP_INFO=true:
+ layout.bounds(new Rect(0, 109, 480, 800-109));
+ button1.bounds(new Rect(182, 83, 298-182, 155-83));
+ button2.bounds(new Rect(122, 155, 238-122, 227-155));
+ button3.bounds(new Rect(238, 155, 354-238, 227-155));
+ hspacer.bounds(new Rect(0, 0, 122, 2));
+ vspacer.bounds(new Rect(0, 0, 2, 155));
+
+ GridModel model = GridModel.get(new LayoutTestBase.TestRulesEngine(targetNode.getFqcn()),
+ targetNode, null);
+ assertEquals(3, model.declaredColumnCount);
+ assertEquals(3, model.actualColumnCount);
+ assertEquals(2, model.actualRowCount);
+
+ ViewData view = model.getView(button1);
+ assertNotNull(view);
+ assertEquals(0, view.column);
+ assertEquals(3, view.columnSpan);
+ assertEquals("3", view.node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN_SPAN));
+
+ model.splitColumn(3, false, 53, 318);
+ assertEquals(0, view.column);
+ assertEquals(4, view.columnSpan);
+ assertEquals("4", view.node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN_SPAN));
+ }
+
+ public void testInsert3_BROKEN() throws Exception {
+ // Check that when we insert a new gap column near an existing column, the
+ // view in that new column does not get moved
+ String xml =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "<GridLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
+ " android:id=\"@+id/GridLayout1\"\n" +
+ " android:layout_width=\"match_parent\"\n" +
+ " android:layout_height=\"match_parent\"\n" +
+ " android:columnCount=\"3\"\n" +
+ " android:orientation=\"vertical\" >\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button1\"\n" +
+ " android:layout_column=\"0\"\n" +
+ " android:layout_columnSpan=\"3\"\n" +
+ " android:layout_gravity=\"center_horizontal|bottom\"\n" +
+ " android:layout_row=\"0\"\n" +
+ " android:text=\"Button\" />\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button2\"\n" +
+ " android:layout_column=\"1\"\n" +
+ " android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"1\"\n" +
+ " android:text=\"Button\" />\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button3\"\n" +
+ " android:layout_column=\"2\"\n" +
+ " android:layout_gravity=\"left|top\"\n" +
+ " android:layout_row=\"1\"\n" +
+ " android:text=\"Button\" />\n" +
+ "\n" +
+ " <Space\n" +
+ " android:id=\"@+id/spacer_393\"\n" +
+ " android:layout_width=\"81dp\"\n" +
+ " android:layout_height=\"1dp\"\n" +
+ " android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ "\n" +
+ " \n" +
+ " <Space\n" +
+ " android:id=\"@+id/spacer_397\"\n" +
+ " android:layout_width=\"1dp\"\n" +
+ " android:layout_height=\"103dp\"\n" +
+ " android:layout_column=\"0\"\n" +
+ " android:layout_row=\"0\" />\n" +
+ "\n" +
+ " \n" +
+ "</GridLayout>";
+
+ TestNode targetNode = TestNode.createFromXml(xml);
+ assertNotNull(targetNode);
+
+ TestNode layout = TestNode.findById(targetNode, "@+id/GridLayout1");
+ TestNode button1 = TestNode.findById(targetNode, "@+id/button1");
+ TestNode button2 = TestNode.findById(targetNode, "@+id/button2");
+ TestNode button3 = TestNode.findById(targetNode, "@+id/button3");
+ TestNode hspacer = TestNode.findById(targetNode, "@+id/spacer_393");
+ TestNode vspacer = TestNode.findById(targetNode, "@+id/spacer_397");
+ assertNotNull(layout);
+ assertNotNull(button1);
+ assertNotNull(button2);
+ assertNotNull(button3);
+ assertNotNull(hspacer);
+
+ // Obtained by setting ViewHierarchy.DUMP_INFO=true:
+ layout.bounds(new Rect(0, 109, 480, 800-109));
+ button1.bounds(new Rect(182, 83, 298-182, 155-83));
+ button2.bounds(new Rect(122, 155, 238-122, 227-155));
+ button3.bounds(new Rect(238, 155, 354-238, 227-155));
+ hspacer.bounds(new Rect(0, 0, 122, 2));
+ vspacer.bounds(new Rect(0, 0, 2, 155));
+
+ GridModel model = GridModel.get(new LayoutTestBase.TestRulesEngine(targetNode.getFqcn()),
+ targetNode, null);
+ assertEquals(3, model.declaredColumnCount);
+ assertEquals(3, model.actualColumnCount);
+ assertEquals(2, model.actualRowCount);
+
+ ViewData view = model.getView(button3);
+ assertNotNull(view);
+ assertEquals(2, view.column);
+ assertEquals(1, view.columnSpan);
+ assertNull("1", view.node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN_SPAN));
+
+ model.splitColumn(2, true, 10, 253);
+ // TODO: Finish this test: Assert that the cells are in the right place
+ //assertEquals(4, view.column);
+ //assertEquals(1, view.columnSpan);
+ //assertEquals("4", view.node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN_SPAN));
+ fail("Finish test");
}
}