diff options
author | Tor Norbye <tnorbye@google.com> | 2012-04-03 16:32:53 -0700 |
---|---|---|
committer | Tor Norbye <tnorbye@google.com> | 2012-06-12 15:11:56 -0700 |
commit | fb6d52d71a7461cb1ac4149f4f71d6a63e7436da (patch) | |
tree | ddae10c083d2532d6b340f8b5699630c9c24142b /eclipse | |
parent | 9208fab7341aebfcf800fa9a293e06ec803927de (diff) | |
download | sdk-fb6d52d71a7461cb1ac4149f4f71d6a63e7436da.zip sdk-fb6d52d71a7461cb1ac4149f4f71d6a63e7436da.tar.gz sdk-fb6d52d71a7461cb1ac4149f4f71d6a63e7436da.tar.bz2 |
GridLayout support work
Lots of miscellaneous fixes to the GridLayout support in ADT. The
changes include using the GridLayout state (via reflection) to
populate the model; caching the grid model for performance, and a
bunch of fixes to the code which handles insertions and
removals. There are also some new unit tests. This is not done, but is
an improvement over the current state.
Change-Id: I4851153d3e409630c2d2024c4894d1ad1535fb47
Diffstat (limited to 'eclipse')
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"); } } |