From 4cd282c7b21dc06c2d2d02748278f07c94282fc1 Mon Sep 17 00:00:00 2001 From: Tor Norbye Date: Wed, 7 Sep 2011 11:28:54 -0700 Subject: [PATCH] Grid Layout and Convert to Grid Layout improvements First, some improvements to Grid Layout handling: (1) When dropping a new widget, look up the sizing metadata and use it to determine what fill gravity to set. For example, a button will use gravity left, and a text field will use gravity fill_horizontal. (2) Don't warn about reflection problems when failing to find GridLayout layout data; this probably means the layout is being attempted opened in an unsupporting SDK. There's a fallback case to compute the data instead already. Second, improvements to the conversion to GridLayout handling: (1) It now looks at the layout_gravity values to see whether each row and column is flexible, and if there's no flexible column in each of the horizontal and vertical dimensions, it will insert a special element to absorb any available extra space. This avoids constraints warnings from GridLayout. (2) It treats layout_width or layout_height attributes of match_parent or fill_parent as the same as a fill gravity (and removes it) and uses this in the flexibility computation above. (3) It removes unsupported layout params for all children (earlier this would only remove unsupported layout params on the direct children of the layout, which isn't enough when a hierarchy is being flattened.) (4) It's smarter about computing implicit rows and columns, so it avoids writing out redundant layout_row and layout_column attributes in some cases. (5) It avoids throwing refactoring errors in cases where an attribute is removed twice (6) Fixes a bug where the root layout was included when computing the set of used x and y coordinates, which means you'd often end up with a blank row and column 0. (7) Various refactoring to make the code cleaner. (8) More unit tests and updates to existing unit tests to reflect the new behavior such as an absorbing spacer and removal of redundant attributes. Change-Id: Iee44c3ca749eefc107b83545669cc9d7f84615b1 --- eclipse/dictionary.txt | 1 + .../android/ide/common/layout/GridLayoutRule.java | 68 ++++- .../ide/common/layout/grid/GridDropHandler.java | 5 + .../android/ide/common/layout/grid/GridModel.java | 7 +- .../refactoring/ChangeLayoutRefactoring.java | 58 ++-- .../layout/refactoring/GridLayoutConverter.java | 305 ++++++++++++++++++--- .../RelativeLayoutConversionHelper.java | 42 ++- .../refactoring/ChangeLayoutRefactoringTest.java | 4 + .../testdata/sample11-expected-insertSpacer.xml | 59 ++++ .../layout/refactoring/testdata/sample11.info | 11 + .../layout/refactoring/testdata/sample11.xml | 92 +++++++ .../testdata/sample1a-expected-gridLayout1.xml | 18 +- .../testdata/sample2-expected-gridLayout2.xml | 15 +- .../testdata/sample5-expected-gridLayout5.xml | 3 +- .../testdata/sample9-expected-convertToGrid.xml | 13 +- 15 files changed, 611 insertions(+), 90 deletions(-) create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample11-expected-insertSpacer.xml create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample11.info create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample11.xml diff --git a/eclipse/dictionary.txt b/eclipse/dictionary.txt index b9f31069b..65a89b37b 100644 --- a/eclipse/dictionary.txt +++ b/eclipse/dictionary.txt @@ -232,6 +232,7 @@ stateful stateless stderr stdout +stretchable stretchiness struct styleable 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 0633aa7ea..baebfad3f 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 @@ -20,7 +20,12 @@ import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_GRAVITY; import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION; import static com.android.ide.common.layout.LayoutConstants.FQCN_SPACE; +import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL; +import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_HORIZONTAL; +import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_VERTICAL; +import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_LEFT; import static com.android.ide.common.layout.LayoutConstants.VALUE_HORIZONTAL; +import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE; import com.android.ide.common.api.DrawingStyle; import com.android.ide.common.api.DropFeedback; @@ -30,7 +35,10 @@ import com.android.ide.common.api.IGraphics; import com.android.ide.common.api.IMenuCallback; import com.android.ide.common.api.INode; import com.android.ide.common.api.INodeHandler; +import com.android.ide.common.api.IViewMetadata; +import com.android.ide.common.api.IViewMetadata.FillPreference; import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.InsertType; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; import com.android.ide.common.api.RuleAction; @@ -89,6 +97,10 @@ public class GridLayoutRule extends BaseLayoutRule { */ public static final double MAX_CELL_DIFFERENCE = 1.2; + /** Whether debugging diagnostics is available in the toolbar */ + private static final boolean CAN_DEBUG = + VALUE_TRUE.equals(System.getenv("ADT_DEBUG_GRIDLAYOUT")); //$NON-NLS-1$ + private static final String ACTION_ADD_ROW = "_addrow"; //$NON-NLS-1$ private static final String ACTION_REMOVE_ROW = "_removerow"; //$NON-NLS-1$ private static final String ACTION_ADD_COL = "_addcol"; //$NON-NLS-1$ @@ -219,8 +231,10 @@ public class GridLayoutRule extends BaseLayoutRule { sShowStructure, actionCallback, ICON_SHOW_GRID, 200, false)); // Temporary: Diagnostics for GridLayout - actions.add(RuleAction.createToggle(ACTION_DEBUG, "Debug", - sDebugGridLayout, actionCallback, null, 210, false)); + if (CAN_DEBUG) { + actions.add(RuleAction.createToggle(ACTION_DEBUG, "Debug", + sDebugGridLayout, actionCallback, null, 210, false)); + } } /** @@ -287,6 +301,56 @@ public class GridLayoutRule extends BaseLayoutRule { } @Override + public void onChildInserted(INode node, INode parent, InsertType insertType) { + if (insertType == InsertType.MOVE_WITHIN) { + // Don't adjust widths/heights/weights when just moving within a single layout + 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) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_GRAVITY, gravity); + } + } + + /** + * Computes the default gravity to be used for a widget of the given fill + * preference when added to a grid layout + * + * @param fill the fill preference for the widget + * @return the gravity value, or null, to be set on the widget + */ + public static String computeDefaultGravity(FillPreference fill) { + String horizontal = GRAVITY_VALUE_LEFT; + String vertical = null; + if (fill.fillHorizontally(true /*verticalContext*/)) { + horizontal = GRAVITY_VALUE_FILL_HORIZONTAL; + } + if (fill.fillVertically(true /*verticalContext*/)) { + vertical = GRAVITY_VALUE_FILL_VERTICAL; + } + String gravity; + if (horizontal == GRAVITY_VALUE_FILL_HORIZONTAL + && vertical == GRAVITY_VALUE_FILL_VERTICAL) { + gravity = GRAVITY_VALUE_FILL; + } else if (vertical != null) { + gravity = horizontal + '|' + vertical; + } else { + gravity = horizontal; + } + + return gravity; + } + + @Override public void onRemovingChildren(List deleted, INode parent) { super.onRemovingChildren(deleted, parent); 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 dba9ce782..9f8e0b114 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 @@ -679,6 +679,11 @@ public class GridDropHandler { newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW_SPAN, Integer.toString(rowSpan)); } + // Ensure that we don't store columnCount=0 + if (mGrid.actualColumnCount == 0) { + mGrid.layout.setAttribute(ANDROID_URI, ATTR_COLUMN_COUNT, VALUE_1); + } + return newChild; } 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 4924cc0dc..a54d467aa 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 @@ -44,7 +44,6 @@ import com.android.ide.common.api.IViewMetadata; import com.android.ide.common.api.Margins; import com.android.ide.common.api.Rect; import com.android.ide.common.layout.GridLayoutRule; -import com.android.ide.eclipse.adt.AdtPlugin; import com.android.util.Pair; import java.io.PrintWriter; @@ -697,10 +696,10 @@ public class GridModel { int[] ys = (int[]) verticalLocations; return Pair.of(xs, ys); } catch (Throwable t) { - AdtPlugin.log(t, null); // TODO: Add to API! + // Probably trying to show a GridLayout on a platform that does not support it. + // Return null to indicate that the grid bounds must be computed from view bounds. + return null; } - - return null; } /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java index d95efcff8..d95bf2c49 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java @@ -40,6 +40,7 @@ import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; import static com.android.ide.eclipse.adt.AdtConstants.EXT_XML; import com.android.annotations.VisibleForTesting; +import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; @@ -53,6 +54,7 @@ import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.viewers.ITreeSelection; @@ -60,6 +62,7 @@ import org.eclipse.ltk.core.refactoring.Change; import org.eclipse.ltk.core.refactoring.Refactoring; import org.eclipse.ltk.core.refactoring.RefactoringStatus; import org.eclipse.ltk.core.refactoring.TextFileChange; +import org.eclipse.text.edits.MalformedTreeException; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; @@ -276,7 +279,9 @@ public class ChangeLayoutRefactoring extends VisualRefactoring { } } else if (newType.equals(FQCN_GRID_LAYOUT)) { convertAnyToGridLayout(rootEdit); - removeUndefinedAttrs(rootEdit, layout); + // Layout attributes on children have already been removed as part of conversion + // during the flattening + removeUndefinedAttrs(rootEdit, layout, false /*removeLayoutAttrs*/); } else if (oldType.equals(FQCN_RELATIVE_LAYOUT) && newType.equals(FQCN_LINEAR_LAYOUT)) { convertRelativeToLinear(rootEdit); removeUndefinedAttrs(rootEdit, layout); @@ -497,29 +502,46 @@ public class ChangeLayoutRefactoring extends VisualRefactoring { * children */ private void removeUndefinedAttrs(MultiTextEdit rootEdit, Element layout) { + removeUndefinedAttrs(rootEdit, layout, true /*removeLayoutAttrs*/); + } + + private void removeUndefinedAttrs(MultiTextEdit rootEdit, Element layout, + boolean removeLayoutAttrs) { ViewElementDescriptor descriptor = getElementDescriptor(mTypeFqcn); if (descriptor == null) { return; } - Set defined = new HashSet(); - AttributeDescriptor[] layoutAttributes = descriptor.getLayoutAttributes(); - for (AttributeDescriptor attribute : layoutAttributes) { - defined.add(attribute.getXmlLocalName()); - } + if (removeLayoutAttrs) { + Set defined = new HashSet(); + AttributeDescriptor[] layoutAttributes = descriptor.getLayoutAttributes(); + for (AttributeDescriptor attribute : layoutAttributes) { + defined.add(attribute.getXmlLocalName()); + } - NodeList children = layout.getChildNodes(); - for (int i = 0, n = children.getLength(); i < n; i++) { - Node node = children.item(i); - if (node.getNodeType() == Node.ELEMENT_NODE) { - Element child = (Element) node; + NodeList children = layout.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node node = children.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element child = (Element) node; - List attributes = findLayoutAttributes(child); - for (Attr attribute : attributes) { - String name = attribute.getLocalName(); - if (!defined.contains(name)) { - // Remove it - removeAttribute(rootEdit, child, attribute.getNamespaceURI(), name); + List attributes = findLayoutAttributes(child); + for (Attr attribute : attributes) { + String name = attribute.getLocalName(); + if (!defined.contains(name)) { + // Remove it + try { + removeAttribute(rootEdit, child, attribute.getNamespaceURI(), name); + } catch (MalformedTreeException mte) { + // Sometimes refactoring has modified attribute; not removing + // it is non-fatal so just warn instead of letting refactoring + // operation abort + AdtPlugin.log(IStatus.WARNING, + "Could not remove unsupported attribute %1$s; " + //$NON-NLS-1$ + "already modified during refactoring?", //$NON-NLS-1$ + attribute.getLocalName()); + } + } } } } @@ -527,7 +549,7 @@ public class ChangeLayoutRefactoring extends VisualRefactoring { // Also remove the unavailable attributes (not layout attributes) on the // converted element - defined = new HashSet(); + Set defined = new HashSet(); AttributeDescriptor[] attributes = descriptor.getAttributes(); for (AttributeDescriptor attribute : attributes) { defined.add(attribute.getXmlLocalName()); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java index e33a12399..6dca1ddce 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java @@ -25,32 +25,48 @@ import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_RI import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_TOP; 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_HEIGHT; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX; 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_LAYOUT_WIDTH; import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION; +import static com.android.ide.common.layout.LayoutConstants.FQCN_GRID_LAYOUT; +import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL; +import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_HORIZONTAL; +import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_VERTICAL; import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX; import static com.android.ide.common.layout.LayoutConstants.LINEAR_LAYOUT; import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; import static com.android.ide.common.layout.LayoutConstants.RADIO_GROUP; import static com.android.ide.common.layout.LayoutConstants.RELATIVE_LAYOUT; +import static com.android.ide.common.layout.LayoutConstants.SPACE; import static com.android.ide.common.layout.LayoutConstants.TABLE_LAYOUT; import static com.android.ide.common.layout.LayoutConstants.TABLE_ROW; +import static com.android.ide.common.layout.LayoutConstants.VALUE_FILL_PARENT; import static com.android.ide.common.layout.LayoutConstants.VALUE_HORIZONTAL; +import static com.android.ide.common.layout.LayoutConstants.VALUE_MATCH_PARENT; import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL; import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; +import com.android.ide.common.api.IViewMetadata.FillPreference; import com.android.ide.common.layout.BaseLayoutRule; +import com.android.ide.common.layout.GridLayoutRule; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository; import org.eclipse.core.runtime.IStatus; import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.text.edits.InsertEdit; +import org.eclipse.text.edits.MalformedTreeException; import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; @@ -91,6 +107,10 @@ class GridLayoutConverter { private final ChangeLayoutRefactoring mRefactoring; private final CanvasViewInfo mRootView; + private List mViews; + private String mNamespace; + private int mColumnCount; + /** Creates a new {@link GridLayoutConverter} */ GridLayoutConverter(ChangeLayoutRefactoring refactoring, Element layout, boolean flatten, MultiTextEdit rootEdit, CanvasViewInfo rootView) { @@ -115,12 +135,158 @@ class GridLayoutConverter { } // Study the layout and get information about how to place individual elements - GridModel edgeList = new GridModel(layoutView); - List views = edgeList.getViews(); - deleteRemovedElements(edgeList.getDeletedElements()); + GridModel gridModel = new GridModel(layoutView, mLayout, mFlatten); + mViews = gridModel.getViews(); + mColumnCount = gridModel.computeColumnCount(); + + deleteRemovedElements(gridModel.getDeletedElements()); + mNamespace = mRefactoring.getAndroidNamespacePrefix(); + + processGravities(); + + // Insert space views if necessary + insertStretchableSpans(); // Create/update relative layout constraints - assignGridAttributes(views); + assignGridAttributes(); + + removeUndefinedAttrs(); + + if (mColumnCount > 0) { + mRefactoring.setAttribute(mRootEdit, mLayout, ANDROID_URI, + mNamespace, ATTR_COLUMN_COUNT, Integer.toString(mColumnCount)); + } + } + + private void insertStretchableSpans() { + // Look at the rows and columns and determine if we need to have a stretchable + // row and/or a stretchable column in the layout. + // In a GridLayout, a row or column is stretchable if it defines a gravity (regardless + // of what the gravity is -- in other words, a column is not just stretchable if it + // has gravity=fill but also if it has gravity=left). Furthermore, ALL the elements + // in the row/column have to be stretchable for the overall row/column to be + // considered stretchable. + + // Map from row index to boolean for "is the row fixed/inflexible?" + Map rowFixed = new HashMap(); + Map columnFixed = new HashMap(); + for (View view : mViews) { + if (view.mElement == mLayout) { + continue; + } + + int gravity = RelativeLayoutConversionHelper.getGravity(view.mGravity, 0); + if ((gravity & RelativeLayoutConversionHelper.GRAVITY_HORIZ_MASK) == 0) { + columnFixed.put(view.mCol, true); + } else if (!columnFixed.containsKey(view.mCol)) { + columnFixed.put(view.mCol, false); + } + if ((gravity & RelativeLayoutConversionHelper.GRAVITY_VERT_MASK) == 0) { + rowFixed.put(view.mRow, true); + } else if (!rowFixed.containsKey(view.mRow)) { + rowFixed.put(view.mRow, false); + } + } + + boolean hasStretchableRow = false; + boolean hasStretchableColumn = false; + for (boolean fixed : rowFixed.values()) { + if (!fixed) { + hasStretchableRow = true; + } + } + for (boolean fixed : columnFixed.values()) { + if (!fixed) { + hasStretchableColumn = true; + } + } + + if (!hasStretchableRow || !hasStretchableColumn) { + // Insert to hold stretchable space + // TODO: May also have to increment column count! + int offset = 0; // WHERE? + + if (mLayout instanceof IndexedRegion) { + IndexedRegion region = (IndexedRegion) mLayout; + int end = region.getEndOffset(); + // TODO: Look backwards for the ") ? + end -= (mLayout.getTagName().length() + 3); // 3: <, /, > + offset = end; + } + + int row = rowFixed.size(); + int column = columnFixed.size(); + StringBuilder sb = new StringBuilder(64); + String tag = SPACE; + sb.append('<').append(tag).append(' '); + String gravity; + if (!hasStretchableRow && !hasStretchableColumn) { + gravity = GRAVITY_VALUE_FILL; + } else if (!hasStretchableRow) { + gravity = GRAVITY_VALUE_FILL_VERTICAL; + } else { + assert !hasStretchableColumn; + gravity = GRAVITY_VALUE_FILL_HORIZONTAL; + } + + sb.append(mNamespace).append(':'); + sb.append(ATTR_LAYOUT_GRAVITY).append('=').append('"').append(gravity); + sb.append('"').append(' '); + + sb.append(mNamespace).append(':'); + sb.append(ATTR_LAYOUT_ROW).append('=').append('"').append(Integer.toString(row)); + sb.append('"').append(' '); + + sb.append(mNamespace).append(':'); + sb.append(ATTR_LAYOUT_COLUMN).append('=').append('"').append(Integer.toString(column)); + sb.append('"').append('/').append('>'); + + String space = sb.toString(); + InsertEdit replace = new InsertEdit(offset, space); + mRootEdit.addChild(replace); + + mColumnCount++; + } + } + + private void removeUndefinedAttrs() { + ViewElementDescriptor descriptor = mRefactoring.getElementDescriptor(FQCN_GRID_LAYOUT); + if (descriptor == null) { + return; + } + + Set defined = new HashSet(); + AttributeDescriptor[] layoutAttributes = descriptor.getLayoutAttributes(); + for (AttributeDescriptor attribute : layoutAttributes) { + defined.add(attribute.getXmlLocalName()); + } + + for (View view : mViews) { + Element child = view.mElement; + + List attributes = mRefactoring.findLayoutAttributes(child); + for (Attr attribute : attributes) { + String name = attribute.getLocalName(); + if (!defined.contains(name)) { + // Remove it + try { + mRefactoring.removeAttribute(mRootEdit, child, attribute.getNamespaceURI(), + name); + } catch (MalformedTreeException mte) { + // Sometimes refactoring has modified attribute; not + // removing + // it is non-fatal so just warn instead of letting + // refactoring + // operation abort + AdtPlugin.log(IStatus.WARNING, + "Could not remove unsupported attribute %1$s; " + //$NON-NLS-1$ + "already modified during refactoring?", //$NON-NLS-1$ + attribute.getLocalName()); + } + } + } + } } /** Removes any elements targeted for deletion */ @@ -136,31 +302,23 @@ class GridLayoutConverter { /** * Creates refactoring edits which adds or updates the grid attributes */ - private void assignGridAttributes(List views) { - String namespace = mRefactoring.getAndroidNamespacePrefix(); - + private void assignGridAttributes() { // We always convert to horizontal grid layouts for now mRefactoring.setAttribute(mRootEdit, mLayout, ANDROID_URI, - namespace, ATTR_ORIENTATION, VALUE_HORIZONTAL); - - int columnCount = computeColumnCount(views); - if (columnCount > 0) { - mRefactoring.setAttribute(mRootEdit, mLayout, ANDROID_URI, - namespace, ATTR_COLUMN_COUNT, Integer.toString(columnCount)); - } + mNamespace, ATTR_ORIENTATION, VALUE_HORIZONTAL); - assignCellAttributes(views, namespace, columnCount); + assignCellAttributes(); } /** * Assign cell attributes to the table, skipping those that will be implied * by the grid model */ - private void assignCellAttributes(List views, String namespace, int columnCount) { + private void assignCellAttributes() { int implicitRow = 0; int implicitColumn = 0; int nextRow = 0; - for (View view : views) { + for (View view : mViews) { Element element = view.getElement(); if (element == mLayout) { continue; @@ -169,15 +327,18 @@ class GridLayoutConverter { int row = view.getRow(); int column = view.getColumn(); - if (row != implicitRow) { + if (column != implicitColumn && (implicitColumn > 0 || implicitRow > 0)) { mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI, - namespace, ATTR_LAYOUT_ROW, Integer.toString(row)); - implicitRow = row; + mNamespace, ATTR_LAYOUT_COLUMN, Integer.toString(column)); + if (column < implicitColumn) { + implicitRow++; + } + implicitColumn = column; } - if (column != implicitColumn) { + if (row != implicitRow) { mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI, - namespace, ATTR_LAYOUT_COLUMN, Integer.toString(column)); - implicitColumn = column; + mNamespace, ATTR_LAYOUT_ROW, Integer.toString(row)); + implicitRow = row; } int rowSpan = view.getRowSpan(); @@ -186,11 +347,11 @@ class GridLayoutConverter { if (rowSpan > 1) { mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI, - namespace, ATTR_LAYOUT_ROW_SPAN, Integer.toString(rowSpan)); + mNamespace, ATTR_LAYOUT_ROW_SPAN, Integer.toString(rowSpan)); } if (columnSpan > 1) { mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI, - namespace, ATTR_LAYOUT_COLUMN_SPAN, + mNamespace, ATTR_LAYOUT_COLUMN_SPAN, Integer.toString(columnSpan)); } nextRow = Math.max(nextRow, row + rowSpan); @@ -212,7 +373,7 @@ class GridLayoutConverter { } implicitColumn += columnSpan; - if (implicitColumn >= columnCount) { + if (implicitColumn >= mColumnCount) { implicitColumn = 0; assert nextRow > implicitRow; implicitRow = nextRow; @@ -220,23 +381,56 @@ class GridLayoutConverter { } } - /** Compute column count */ - private int computeColumnCount(List views) { - int columnCount = 0; - for (View view : views) { - if (view.getElement() == mLayout) { + private void processGravities() { + for (View view : mViews) { + Element element = view.getElement(); + if (element == mLayout) { continue; } - int column = view.getColumn(); - int columnSpan = view.getColumnSpan(); - if (column + columnSpan > columnCount) { - columnCount = column + columnSpan; + Attr width = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); + Attr height = element.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); + String gravity = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY); + String newGravity = null; + if (width != null && (VALUE_MATCH_PARENT.equals(width.getValue()) || + VALUE_FILL_PARENT.equals(width.getValue()))) { + mRefactoring.removeAttribute(mRootEdit, width); + newGravity = gravity = GRAVITY_VALUE_FILL_HORIZONTAL; + } + if (height != null && (VALUE_MATCH_PARENT.equals(height.getValue()) || + VALUE_FILL_PARENT.equals(height.getValue()))) { + mRefactoring.removeAttribute(mRootEdit, height); + if (newGravity == GRAVITY_VALUE_FILL_HORIZONTAL) { + newGravity = GRAVITY_VALUE_FILL; + } else { + newGravity = GRAVITY_VALUE_FILL_VERTICAL; + } + gravity = newGravity; } + + if (gravity == null || gravity.length() == 0) { + ElementDescriptor descriptor = view.mInfo.getUiViewNode().getDescriptor(); + if (descriptor instanceof ViewElementDescriptor) { + ViewElementDescriptor viewDescriptor = (ViewElementDescriptor) descriptor; + String fqcn = viewDescriptor.getFullClassName(); + FillPreference fill = ViewMetadataRepository.get().getFillPreference(fqcn); + gravity = GridLayoutRule.computeDefaultGravity(fill); + if (gravity != null) { + newGravity = gravity; + } + } + } + + if (newGravity != null) { + mRefactoring.setAttribute(mRootEdit, element, ANDROID_URI, + mNamespace, ATTR_LAYOUT_GRAVITY, newGravity); + } + + view.mGravity = newGravity != null ? newGravity : gravity; } - return columnCount; } + /** Converts 0dip values in layout_width and layout_height to wrap_content instead */ private void convert0dipToWrapContent(Element child) { // Must convert layout_height="0dip" to layout_height="wrap_content". @@ -304,6 +498,7 @@ class GridLayoutConverter { private int mX2; private int mY2; private CanvasViewInfo mInfo; + private String mGravity; public View(CanvasViewInfo view, Element element) { mInfo = view; @@ -404,12 +599,17 @@ class GridLayoutConverter { } /** Grid model for the views found in the view hierarchy, partitioned into rows and columns */ - private class GridModel { + private static class GridModel { private final List mViews = new ArrayList(); private final List mDelete = new ArrayList(); private final Map mElementToView = new HashMap(); + private Element mLayout; + private boolean mFlatten; + + GridModel(CanvasViewInfo view, Element layout, boolean flatten) { + mLayout = layout; + mFlatten = flatten; - GridModel(CanvasViewInfo view) { scan(view, true); analyzeKnownLayouts(); initializeColumns(); @@ -437,13 +637,34 @@ class GridLayoutConverter { } /** + * Compute and return column count + * + * @return the column count + */ + public int computeColumnCount() { + int columnCount = 0; + for (View view : mViews) { + if (view.getElement() == mLayout) { + continue; + } + + int column = view.getColumn(); + int columnSpan = view.getColumnSpan(); + if (column + columnSpan > columnCount) { + columnCount = column + columnSpan; + } + } + return columnCount; + } + + /** * Initializes the column and columnSpan attributes of the views */ private void initializeColumns() { // Now initialize table view row, column and spans Map> mColumnViews = new HashMap>(); for (View view : mViews) { - if (view == mLayout) { + if (view.mElement == mLayout) { continue; } int x = view.getLeftEdge(); @@ -470,7 +691,7 @@ class GridLayoutConverter { } // Initialize column spans for (View view : mViews) { - if (view == mLayout) { + if (view.mElement == mLayout) { continue; } int index = Collections.binarySearch(columnOffsets, view.getRightEdge()); @@ -494,7 +715,7 @@ class GridLayoutConverter { private void initializeRows() { Map> mRowViews = new HashMap>(); for (View view : mViews) { - if (view == mLayout) { + if (view.mElement == mLayout) { continue; } int y = view.getTopEdge(); @@ -522,7 +743,7 @@ class GridLayoutConverter { // Initialize row spans for (View view : mViews) { - if (view == mLayout) { + if (view.mElement == mLayout) { continue; } int index = Collections.binarySearch(rowOffsets, view.getBottomEdge()); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java index dabbca8a7..4344cda25 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java @@ -334,23 +334,39 @@ class RelativeLayoutConversionHelper { } } - private static final int GRAVITY_LEFT = 1 << 0; - private static final int GRAVITY_RIGHT = 1<< 1; - private static final int GRAVITY_CENTER_HORIZ = 1 << 2; - private static final int GRAVITY_FILL_HORIZ = 1 << 3; - private static final int GRAVITY_CENTER_VERT = 1 << 4; - private static final int GRAVITY_FILL_VERT = 1 << 5; - private static final int GRAVITY_TOP = 1 << 6; - private static final int GRAVITY_BOTTOM = 1 << 7; - private static final int GRAVITY_HORIZ_MASK = GRAVITY_CENTER_HORIZ | GRAVITY_FILL_HORIZ + public static final int GRAVITY_LEFT = 1 << 0; + public static final int GRAVITY_RIGHT = 1<< 1; + public static final int GRAVITY_CENTER_HORIZ = 1 << 2; + public static final int GRAVITY_FILL_HORIZ = 1 << 3; + public static final int GRAVITY_CENTER_VERT = 1 << 4; + public static final int GRAVITY_FILL_VERT = 1 << 5; + public static final int GRAVITY_TOP = 1 << 6; + public static final int GRAVITY_BOTTOM = 1 << 7; + public static final int GRAVITY_HORIZ_MASK = GRAVITY_CENTER_HORIZ | GRAVITY_FILL_HORIZ | GRAVITY_LEFT | GRAVITY_RIGHT; - private static final int GRAVITY_VERT_MASK = GRAVITY_CENTER_VERT | GRAVITY_FILL_VERT + public static final int GRAVITY_VERT_MASK = GRAVITY_CENTER_VERT | GRAVITY_FILL_VERT | GRAVITY_TOP | GRAVITY_BOTTOM; - /** Returns the gravity of the given element */ - private static int getGravity(Element element) { - int gravity = GRAVITY_LEFT | GRAVITY_TOP; + /** + * Returns the gravity of the given element + * + * @param element the element to look up the gravity for + * @return a bit mask corresponding to the selected gravities + */ + public static int getGravity(Element element) { String gravityString = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY); + return getGravity(gravityString, GRAVITY_LEFT | GRAVITY_TOP); + } + + /** + * Returns the gravity bitmask for the given gravity string description + * + * @param gravityString the gravity string description + * @param defaultMask the default/initial bitmask to start with + * @return a bitmask corresponding to the gravity description + */ + public static int getGravity(String gravityString, int defaultMask) { + int gravity = defaultMask; if (gravityString != null && gravityString.length() > 0) { String[] anchors = gravityString.split("\\|"); //$NON-NLS-1$ for (String anchor : anchors) { diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoringTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoringTest.java index 5236dc284..bcbf46172 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoringTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoringTest.java @@ -98,6 +98,10 @@ public class ChangeLayoutRefactoringTest extends RefactoringTest { checkRefactoring(FQCN_LINEAR_LAYOUT, "sample10.xml", true, "android:orientation=vertical"); } + public void testInsertSpacer() throws Exception { + checkRefactoring(FQCN_GRID_LAYOUT, "sample11.xml", true); + } + private void checkRefactoring(String newLayoutType, String basename, boolean flatten) throws Exception { checkRefactoring(newLayoutType, basename, flatten, null); diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample11-expected-insertSpacer.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample11-expected-insertSpacer.xml new file mode 100644 index 000000000..d10626a90 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample11-expected-insertSpacer.xml @@ -0,0 +1,59 @@ + + + + + @@ -44,8 +49,13 @@ + + \ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2-expected-gridLayout2.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2-expected-gridLayout2.xml index 40031a624..8b9056216 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2-expected-gridLayout2.xml +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2-expected-gridLayout2.xml @@ -3,12 +3,13 @@ android:id="@+id/GridLayout1" android:layout_width="match_parent" android:layout_height="match_parent" - android:columnCount="5" + android:columnCount="6" android:orientation="horizontal" > @@ -17,13 +18,14 @@ android:layout_alignParentLeft="true" android:layout_column="0" android:layout_columnSpan="2" - android:layout_row="1" + android:layout_gravity="left" android:text="Button" > + + \ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5-expected-gridLayout5.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5-expected-gridLayout5.xml index 09f8d3689..dd9f18cf2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5-expected-gridLayout5.xml +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5-expected-gridLayout5.xml @@ -8,7 +8,6 @@ diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample9-expected-convertToGrid.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample9-expected-convertToGrid.xml index 15f1c3b72..03ffac714 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample9-expected-convertToGrid.xml +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample9-expected-convertToGrid.xml @@ -3,26 +3,35 @@ android:id="@+id/GridLayout1" android:layout_width="match_parent" android:layout_height="match_parent" - android:columnCount="2" + android:columnCount="3" android:orientation="horizontal" >