diff options
83 files changed, 4953 insertions, 331 deletions
diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java index 7e2e52d..22648a9 100644 --- a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java @@ -418,7 +418,6 @@ public class InstrumentationResultParser extends MultiLineReceiver { * * @see IShellOutputReceiver#isCancelled() */ - @Override public boolean isCancelled() { return mIsCancelled; } diff --git a/eclipse/dictionary.txt b/eclipse/dictionary.txt index 86f5e2f..367970e 100644 --- a/eclipse/dictionary.txt +++ b/eclipse/dictionary.txt @@ -21,6 +21,7 @@ attr attrs avd avds +backfill backport backported basename @@ -44,6 +45,7 @@ clueless codebase codename codenames +colspan combo combobox combos @@ -85,6 +87,7 @@ env equidistant exec fallback +flux foo foreach fqcn @@ -93,6 +96,7 @@ gen git groovy guava +hardcode hardcoded hardcodes holo @@ -109,6 +113,7 @@ infos init inits inline +inset instanceof instantiatable int @@ -191,7 +196,10 @@ rescales residual resizability resizable +risky rollback +rowspan +rowspans sans scrollable scrollbar diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/GridLayout.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/GridLayout.png Binary files differnew file mode 100644 index 0000000..1aa4165 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/GridLayout.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/Space.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/Space.png Binary files differnew file mode 100644 index 0000000..58afbe4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/Space.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/TextureView.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/TextureView.png Binary files differnew file mode 100644 index 0000000..353b6b7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/TextureView.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DrawingStyle.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DrawingStyle.java index 1f4cd23..199608e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DrawingStyle.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DrawingStyle.java @@ -49,6 +49,16 @@ public enum DrawingStyle { GUIDELINE_DASHED, /** + * The style used to draw distance annotations + */ + DISTANCE, + + /** + * The style used to draw grids + */ + GRID, + + /** * The style used for hovered views (e.g. when the mouse is directly on top * of the view) */ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DropFeedback.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DropFeedback.java index 855d8b0..551cb3c 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DropFeedback.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DropFeedback.java @@ -142,6 +142,12 @@ public class DropFeedback { public String errorMessage; /** + * A message to be displayed in a tooltip to the user, which should be short, but + * can be multiple lines (use embedded newlines) + */ + public String tooltip; + + /** * A mask of the currently held keyboard modifier keys - some combination of * {@link #MODIFIER1}, {@link #MODIFIER2}, {@link #MODIFIER3}, or none. */ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IClientRulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IClientRulesEngine.java index eb3f7e7..0853378 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IClientRulesEngine.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IClientRulesEngine.java @@ -179,6 +179,25 @@ public interface IClientRulesEngine { public int pxToDp(int px); /** + * Converts a device independent pixel to a screen pixel for the current screen density + * + * @param dp the device independent pixel dimension + * @return the corresponding pixel dimension + */ + public int dpToPx(int dp); + + /** + * Converts an IDE screen pixel distance to the corresponding layout distance. This + * can be used to draw annotations on the graphics object that should be unaffected by + * the zoom, or handle mouse events within a certain pixel distance regardless of the + * screen zoom. + * + * @param pixels the size in IDE screen pixels + * @return the corresponding pixel distance in the layout coordinate system + */ + public int screenToLayout(int pixels); + + /** * Measure the preferred or actual ("wrap_content") size of the given nodes. * * @param parent the parent whose children should be measured diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IGraphics.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IGraphics.java index f847694..0ee2ef2 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IGraphics.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IGraphics.java @@ -56,6 +56,14 @@ public interface IGraphics { void drawArrow(int x1, int y1, int x2, int y2, int size); /** + * Draws a dot at the given position. + * + * @param x The x coordinate of the dot + * @param y The y coordinate of the dot + */ + void drawPoint(int x, int y); + + /** * Draws a rectangle outline between 2 points, using the current foreground * color and alpha. */ @@ -190,6 +198,9 @@ public interface IGraphics { * This operation requires the operating system's advanced * graphics subsystem which may not be available on some * platforms. + * <p> + * TODO: Consider removing this method; it will usually be ignored because + * most graphics operations apply the alpha from the current drawing style */ void setAlpha(int alpha); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewMetadata.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewMetadata.java index 22fcb50..0687f30 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewMetadata.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewMetadata.java @@ -35,6 +35,13 @@ public interface IViewMetadata { public String getDisplayName(); /** + * Gets the insets for this view + * + * @return the insets for this view + */ + public Margins getInsets(); + + /** * Returns the {@link FillPreference} of this view * * @return the {@link FillPreference} of this view diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewRule.java index 1e37245..a16db28 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewRule.java @@ -178,6 +178,10 @@ public interface IViewRule { /** * Called when drop is released over the target to perform the actual drop. + * <p> + * TODO: Document that this method will be called under an edit lock so you can + * directly manipulate the nodes without wrapping it in an + * {@link INode#editXml(String, INodeHandler)} call */ void onDropped(INode targetNode, IDragElement[] elements, @@ -229,6 +233,21 @@ public interface IViewRule { void onChildInserted(INode child, INode parent, InsertType insertType); /** + * Called when one or more children are about to be deleted by the user. Note that + * children deleted programmatically from view rules (via + * {@link INode#removeChild(INode)}) will not notify about deletion. + * <p> + * Note that this method will be called under an edit lock, so rules can directly + * add/remove nodes and attributes as part of the deletion handling (and their + * actions will be part of the same undo-unit.) + * + * @param deleted a nonempty list of children about to be deleted + * @param parent the parent of the deleted children (which still contains the children + * since this method is called before the deletion is performed) + */ + void onRemovingChildren(List<INode> deleted, INode parent); + + /** * Called by the IDE on the parent layout when a child widget is being resized. This * is called once at the beginning of the resizing operation. A horizontal edge, * or a vertical edge, or both, can be resized simultaneously. diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Margins.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Margins.java index 9e7c1d9..40f44ce 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Margins.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Margins.java @@ -17,7 +17,9 @@ package com.android.ide.common.api; /** - * Set of margins for a node. + * Set of margins - distances to outer left, top, right and bottom edges. These objects + * can be used for both actual <b>margins</b> as well as insets - and in general any + * deltas to the bounds of a rectangle. */ public class Margins { /** The left margin */ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java index de03a19..9a789c6 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java @@ -235,7 +235,11 @@ public class AbsoluteLayoutRule extends BaseLayoutRule { protected String getResizeUpdateMessage(ResizeState resizeState, INode child, INode parent, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { Rect parentBounds = parent.getBounds(); - return String.format("Set bounds to (x = %d, y = %d, width = %s, height = %s)", + if (horizontalEdge == SegmentType.BOTTOM && verticalEdge == SegmentType.RIGHT) { + return super.getResizeUpdateMessage(resizeState, child, parent, newBounds, + horizontalEdge, verticalEdge); + } + return String.format("x=%d, y=%d\nwidth=%s, height=%s", mRulesEngine.pxToDp(newBounds.x - parentBounds.x), mRulesEngine.pxToDp(newBounds.y - parentBounds.y), resizeState.getWidthAttribute(), resizeState.getHeightAttribute()); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java index fa87fa4..1a99385 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java @@ -18,13 +18,37 @@ package com.android.ide.common.layout; import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ABOVE; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BOTTOM; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_LEFT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_RIGHT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_TOP; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_IN_PARENT; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL; +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_MARGIN; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_BOTTOM; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_LEFT; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_RIGHT; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_TOP; +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_TO_LEFT_OF; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_X; +import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_Y; import static com.android.ide.common.layout.LayoutConstants.ATTR_TEXT; import static com.android.ide.common.layout.LayoutConstants.VALUE_FILL_PARENT; import static com.android.ide.common.layout.LayoutConstants.VALUE_MATCH_PARENT; @@ -33,21 +57,21 @@ import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; import com.android.ide.common.api.DrawingStyle; import com.android.ide.common.api.DropFeedback; import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.common.api.IAttributeInfo.Format; import com.android.ide.common.api.IClientRulesEngine; import com.android.ide.common.api.IDragElement; +import com.android.ide.common.api.IDragElement.IDragAttribute; import com.android.ide.common.api.IFeedbackPainter; 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.MenuAction; +import com.android.ide.common.api.MenuAction.ChoiceProvider; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; import com.android.ide.common.api.Segment; import com.android.ide.common.api.SegmentType; -import com.android.ide.common.api.IAttributeInfo.Format; -import com.android.ide.common.api.IDragElement.IDragAttribute; -import com.android.ide.common.api.MenuAction.ChoiceProvider; import com.android.ide.common.layout.relative.MarginType; import com.android.sdklib.SdkConstants; import com.android.util.Pair; @@ -78,7 +102,7 @@ public class BaseLayoutRule extends BaseViewRule { // The Margin layout parameters are available for LinearLayout, FrameLayout, RelativeLayout, // and their subclasses. - protected MenuAction createMarginAction(final INode parentNode, + protected final MenuAction createMarginAction(final INode parentNode, final List<? extends INode> children) { final List<? extends INode> targets = children == null || children.size() == 0 ? @@ -119,7 +143,7 @@ public class BaseLayoutRule extends BaseViewRule { // Both LinearLayout and RelativeLayout have a gravity (but RelativeLayout applies it // to the parent whereas for LinearLayout it's on the children) - protected MenuAction createGravityAction(final List<? extends INode> targets, final + protected final MenuAction createGravityAction(final List<? extends INode> targets, final String attributeName) { if (targets != null && targets.size() > 0) { final INode first = targets.get(0); @@ -254,14 +278,20 @@ public class BaseLayoutRule extends BaseViewRule { // ==== Utility methods used by derived layouts ==== /** - * Draws the bounds of the given elements and all its children elements in - * the canvas with the specified offset. + * Draws the bounds of the given elements and all its children elements in the canvas + * with the specified offset. + * + * @param gc the graphics context + * @param element the element to be drawn + * @param offsetX a horizontal delta to add to the current bounds of the element when + * drawing it + * @param offsetY a vertical delta to add to the current bounds of the element when + * drawing it */ - protected void drawElement(IGraphics gc, IDragElement element, int offsetX, int offsetY) { + public void drawElement(IGraphics gc, IDragElement element, int offsetX, int offsetY) { Rect b = element.getBounds(); if (b.isValid()) { - b = b.copy().offsetBy(offsetX, offsetY); - gc.drawRect(b); + gc.drawRect(b.x + offsetX, b.y + offsetY, b.x + offsetX + b.w, b.y + offsetY + b.h); } for (IDragElement inner : element.getInnerElements()) { @@ -411,28 +441,37 @@ public class BaseLayoutRule extends BaseViewRule { } private static final String[] EXCLUDED_ATTRIBUTES = new String[] { + // Common + ATTR_LAYOUT_GRAVITY, + // from AbsoluteLayout - "layout_x", //$NON-NLS-1$ - "layout_y", //$NON-NLS-1$ + ATTR_LAYOUT_X, + ATTR_LAYOUT_Y, // from RelativeLayout - "layout_above", //$NON-NLS-1$ - "layout_below", //$NON-NLS-1$ - "layout_toLeftOf", //$NON-NLS-1$ - "layout_toRightOf", //$NON-NLS-1$ - "layout_alignBaseline", //$NON-NLS-1$ - "layout_alignTop", //$NON-NLS-1$ - "layout_alignBottom", //$NON-NLS-1$ - "layout_alignLeft", //$NON-NLS-1$ - "layout_alignRight", //$NON-NLS-1$ - "layout_alignParentTop", //$NON-NLS-1$ - "layout_alignParentBottom", //$NON-NLS-1$ - "layout_alignParentLeft", //$NON-NLS-1$ - "layout_alignParentRight", //$NON-NLS-1$ - "layout_alignWithParentMissing", //$NON-NLS-1$ - "layout_centerHorizontal", //$NON-NLS-1$ - "layout_centerInParent", //$NON-NLS-1$ - "layout_centerVertical", //$NON-NLS-1$ + ATTR_LAYOUT_ABOVE, + ATTR_LAYOUT_BELOW, + ATTR_LAYOUT_TO_LEFT_OF, + ATTR_LAYOUT_TO_RIGHT_OF, + ATTR_LAYOUT_ALIGN_BASELINE, + ATTR_LAYOUT_ALIGN_TOP, + ATTR_LAYOUT_ALIGN_BOTTOM, + ATTR_LAYOUT_ALIGN_LEFT, + ATTR_LAYOUT_ALIGN_RIGHT, + ATTR_LAYOUT_ALIGN_PARENT_TOP, + ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, + ATTR_LAYOUT_ALIGN_PARENT_LEFT, + ATTR_LAYOUT_ALIGN_PARENT_RIGHT, + ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING, + ATTR_LAYOUT_CENTER_HORIZONTAL, + ATTR_LAYOUT_CENTER_IN_PARENT, + ATTR_LAYOUT_CENTER_VERTICAL, + + // From GridLayout + ATTR_LAYOUT_ROW, + ATTR_LAYOUT_ROW_SPAN, + ATTR_LAYOUT_COLUMN, + ATTR_LAYOUT_COLUMN_SPAN }; /** @@ -614,84 +653,88 @@ public class BaseLayoutRule extends BaseViewRule { public void paint(IGraphics gc, INode node, DropFeedback feedback) { ResizeState resizeState = (ResizeState) feedback.userData; if (resizeState != null && resizeState.bounds != null) { - gc.useStyle(DrawingStyle.RESIZE_PREVIEW); - Rect b = resizeState.bounds; - gc.drawRect(b); - - if (resizeState.horizontalFillSegment != null) { - gc.useStyle(DrawingStyle.GUIDELINE); - Segment s = resizeState.horizontalFillSegment; - gc.drawLine(s.from, s.at, s.to, s.at); - } - if (resizeState.verticalFillSegment != null) { - gc.useStyle(DrawingStyle.GUIDELINE); - Segment s = resizeState.verticalFillSegment; - gc.drawLine(s.at, s.from, s.at, s.to); - } + paintResizeFeedback(gc, node, resizeState); + } + } + }); + } - if (resizeState.wrapBounds != null) { - gc.useStyle(DrawingStyle.GUIDELINE); - int wrapWidth = resizeState.wrapBounds.w; - int wrapHeight = resizeState.wrapBounds.h; - - // Show the "wrap_content" guideline. - // If we are showing both the wrap_width and wrap_height lines - // then we show at most the rectangle formed by the two lines; - // otherwise we show the entire width of the line - if (resizeState.horizontalEdgeType != null) { - int y = -1; - switch (resizeState.horizontalEdgeType) { - case TOP: - y = b.y + b.h - wrapHeight; - break; - case BOTTOM: - y = b.y + wrapHeight; - break; - default: assert false : resizeState.horizontalEdgeType; - } - if (resizeState.verticalEdgeType != null) { - switch (resizeState.verticalEdgeType) { - case LEFT: - gc.drawLine(b.x + b.w - wrapWidth, y, b.x + b.w, y); - break; - case RIGHT: - gc.drawLine(b.x, y, b.x + wrapWidth, y); - break; - default: assert false : resizeState.verticalEdgeType; - } - } else { - gc.drawLine(b.x, y, b.x + b.w, y); - } - } - if (resizeState.verticalEdgeType != null) { - int x = -1; - switch (resizeState.verticalEdgeType) { - case LEFT: - x = b.x + b.w - wrapWidth; - break; - case RIGHT: - x = b.x + wrapWidth; - break; - default: assert false : resizeState.verticalEdgeType; - } - if (resizeState.horizontalEdgeType != null) { - switch (resizeState.horizontalEdgeType) { - case TOP: - gc.drawLine(x, b.y + b.h - wrapHeight, x, b.y + b.h); - break; - case BOTTOM: - gc.drawLine(x, b.y, x, b.y + wrapHeight); - break; - default: assert false : resizeState.horizontalEdgeType; - } - } else { - gc.drawLine(x, b.y, x, b.y + b.h); - } - } + protected void paintResizeFeedback(IGraphics gc, INode node, ResizeState resizeState) { + gc.useStyle(DrawingStyle.RESIZE_PREVIEW); + Rect b = resizeState.bounds; + gc.drawRect(b); + + if (resizeState.horizontalFillSegment != null) { + gc.useStyle(DrawingStyle.GUIDELINE); + Segment s = resizeState.horizontalFillSegment; + gc.drawLine(s.from, s.at, s.to, s.at); + } + if (resizeState.verticalFillSegment != null) { + gc.useStyle(DrawingStyle.GUIDELINE); + Segment s = resizeState.verticalFillSegment; + gc.drawLine(s.at, s.from, s.at, s.to); + } + + if (resizeState.wrapBounds != null) { + gc.useStyle(DrawingStyle.GUIDELINE); + int wrapWidth = resizeState.wrapBounds.w; + int wrapHeight = resizeState.wrapBounds.h; + + // Show the "wrap_content" guideline. + // If we are showing both the wrap_width and wrap_height lines + // then we show at most the rectangle formed by the two lines; + // otherwise we show the entire width of the line + if (resizeState.horizontalEdgeType != null) { + int y = -1; + switch (resizeState.horizontalEdgeType) { + case TOP: + y = b.y + b.h - wrapHeight; + break; + case BOTTOM: + y = b.y + wrapHeight; + break; + default: assert false : resizeState.horizontalEdgeType; + } + if (resizeState.verticalEdgeType != null) { + switch (resizeState.verticalEdgeType) { + case LEFT: + gc.drawLine(b.x + b.w - wrapWidth, y, b.x + b.w, y); + break; + case RIGHT: + gc.drawLine(b.x, y, b.x + wrapWidth, y); + break; + default: assert false : resizeState.verticalEdgeType; } + } else { + gc.drawLine(b.x, y, b.x + b.w, y); } } - }); + if (resizeState.verticalEdgeType != null) { + int x = -1; + switch (resizeState.verticalEdgeType) { + case LEFT: + x = b.x + b.w - wrapWidth; + break; + case RIGHT: + x = b.x + wrapWidth; + break; + default: assert false : resizeState.verticalEdgeType; + } + if (resizeState.horizontalEdgeType != null) { + switch (resizeState.horizontalEdgeType) { + case TOP: + gc.drawLine(x, b.y + b.h - wrapHeight, x, b.y + b.h); + break; + case BOTTOM: + gc.drawLine(x, b.y, x, b.y + wrapHeight); + break; + default: assert false : resizeState.horizontalEdgeType; + } + } else { + gc.drawLine(x, b.y, x, b.y + b.h); + } + } + } } public static final int getMaxMatchDistance() { @@ -704,6 +747,7 @@ public class BaseLayoutRule extends BaseViewRule { Rect newBounds, int modifierMask) { ResizeState state = (ResizeState) feedback.userData; state.bounds = newBounds; + state.modifierMask = modifierMask; // Match on wrap bounds state.wrapWidth = state.wrapHeight = false; @@ -756,7 +800,7 @@ public class BaseLayoutRule extends BaseViewRule { } } - feedback.message = getResizeUpdateMessage(state, child, parent, + feedback.tooltip = getResizeUpdateMessage(state, child, parent, newBounds, state.horizontalEdgeType, state.verticalEdgeType); } @@ -791,8 +835,14 @@ public class BaseLayoutRule extends BaseViewRule { String width = resizeState.getWidthAttribute(); String height = resizeState.getHeightAttribute(); - // U+00D7: Unicode for multiplication sign - return String.format("Resize to %s \u00D7 %s", width, height); + if (horizontalEdge == null) { + return width; + } else if (verticalEdge == null) { + return height; + } else { + // U+00D7: Unicode for multiplication sign + return String.format("%s \u00D7 %s", width, height); + } } /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java index 920aaf3..625ae34 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java @@ -29,6 +29,7 @@ import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; import com.android.ide.common.api.DropFeedback; import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.common.api.IAttributeInfo.Format; import com.android.ide.common.api.IClientRulesEngine; import com.android.ide.common.api.IDragElement; import com.android.ide.common.api.IGraphics; @@ -42,7 +43,6 @@ import com.android.ide.common.api.MenuAction; 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.api.IAttributeInfo.Format; import java.util.ArrayList; import java.util.Arrays; @@ -99,6 +99,15 @@ public class BaseViewRule implements IViewRule { return null; } + /** + * Returns the {@link IClientRulesEngine} associated with this {@link IViewRule} + * + * @return the {@link IClientRulesEngine} associated with this {@link IViewRule} + */ + public IClientRulesEngine getRulesEngine() { + return mRulesEngine; + } + // === Context Menu === /** @@ -458,7 +467,7 @@ public class BaseViewRule implements IViewRule { * Returns true if the given node is "filled" (e.g. has layout width set to match * parent or fill parent */ - protected boolean isFilled(INode node, String attribute) { + protected final boolean isFilled(INode node, String attribute) { String value = node.getStringAttr(ANDROID_URI, attribute); return VALUE_MATCH_PARENT.equals(value) || VALUE_FILL_PARENT.equals(value); } @@ -469,7 +478,7 @@ public class BaseViewRule implements IViewRule { * * @return match_parent or fill_parent depending on which is supported by the project */ - protected String getFillParentValueName() { + protected final String getFillParentValueName() { return supportsMatchParent() ? VALUE_MATCH_PARENT : VALUE_FILL_PARENT; } @@ -478,7 +487,7 @@ public class BaseViewRule implements IViewRule { * * @return true if the project supports match_parent instead of just fill_parent */ - protected boolean supportsMatchParent() { + protected final boolean supportsMatchParent() { // fill_parent was renamed match_parent in API level 8 return mRulesEngine.getMinApiLevel() >= 8; } @@ -647,7 +656,7 @@ public class BaseViewRule implements IViewRule { * * @return a source attribute to use for sample images, never null */ - protected String getSampleImageSrc() { + protected final String getSampleImageSrc() { // For now, we point to the sample icon which is written into new Android projects // created in ADT. We could alternatively look into the project resources folder // and try to pick something else, or even return some builtin image resource @@ -662,6 +671,9 @@ public class BaseViewRule implements IViewRule { public void onChildInserted(INode node, INode parent, InsertType insertType) { } + public void onRemovingChildren(List<INode> deleted, INode parent) { + } + public static String stripIdPrefix(String id) { if (id == null) { return ""; //$NON-NLS-1$ 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 new file mode 100644 index 0000000..2e28713 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java @@ -0,0 +1,430 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.common.layout; + +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.VALUE_HORIZONTAL; + +import com.android.ide.common.api.DrawingStyle; +import com.android.ide.common.api.DropFeedback; +import com.android.ide.common.api.IDragElement; +import com.android.ide.common.api.IFeedbackPainter; +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.IViewRule; +import com.android.ide.common.api.MenuAction; +import com.android.ide.common.api.MenuAction.OrderedChoices; +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.grid.GridDropHandler; +import com.android.ide.common.layout.grid.GridLayoutPainter; +import com.android.ide.common.layout.grid.GridModel; +import com.android.util.Pair; + +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * An {@link IViewRule} for android.widget.GridLayout which provides designtime + * interaction with GridLayouts. + * <p> + * TODO: + * <ul> + * <li>Handle multi-drag: preserving relative positions and alignments among dragged + * views. + * <li>Handle GridLayouts that have been configured in a vertical orientation. + * <li>Handle free-form editing GridLayouts that have been manually edited rather than + * built up using free-form editing (e.g. they might not follow the same spacing + * convention, might use weights etc) + * <li>Avoid setting row and column numbers on the actual elements if they can be skipped + * to make the XML leaner. + * </ul> + */ +public class GridLayoutRule extends BaseLayoutRule { + /** + * The size of the visual regular grid that we snap to (if {@link #sSnapToGrid} is set + */ + public static final int GRID_SIZE = 16; + + /** Standard gap between views */ + public static final int SHORT_GAP_DP = 16; + + /** + * The preferred margin size, in pixels + */ + public static final int MARGIN_SIZE = 32; + + /** + * Size in screen pixels in the IDE of the gutter shown for new rows and columns (in + * grid mode) + */ + private static final int NEW_CELL_WIDTH = 10; + + /** + * Maximum size of a widget relative to a cell which is allowed to fit into a cell + * (and thereby enlarge it) before it is spread with row or column spans. + */ + public static final double MAX_CELL_DIFFERENCE = 1.2; + + 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$ + private static final String ACTION_REMOVE_COL = "_removecol"; //$NON-NLS-1$ + private static final String ACTION_ORIENTATION = "_orientation"; //$NON-NLS-1$ + private static final String ACTION_SHOW_GRID = "_grid"; //$NON-NLS-1$ + private static final String ACTION_SNAP = "_snap"; //$NON-NLS-1$ + private static final String ACTION_DEBUG = "_debug"; //$NON-NLS-1$ + + private static final URL ICON_HORIZONTAL = GridLayoutRule.class.getResource("hlinear.png"); //$NON-NLS-1$ + private static final URL ICON_VERTICAL = GridLayoutRule.class.getResource("vlinear.png"); //$NON-NLS-1$ + private static final URL ICON_ADD_ROW = GridLayoutRule.class.getResource("addrow.png"); //$NON-NLS-1$ + private static final URL ICON_REMOVE_ROW = GridLayoutRule.class.getResource("removerow.png"); //$NON-NLS-1$ + private static final URL ICON_ADD_COL = GridLayoutRule.class.getResource("addcol.png"); //$NON-NLS-1$ + private static final URL ICON_REMOVE_COL = GridLayoutRule.class.getResource("removecol.png"); //$NON-NLS-1$ + private static final URL ICON_SHOW_GRID = GridLayoutRule.class.getResource("showgrid.png"); //$NON-NLS-1$ + private static final URL ICON_SNAP = GridLayoutRule.class.getResource("snap.png"); //$NON-NLS-1$ + + /** + * Whether the IDE should show diagnostics for debugging the grid layout - including + * spacers visibly in the outline, showing row and column numbers, and so on + */ + public static boolean sDebugGridLayout = false; + + /** Whether the structure (grid model) should be displayed persistently to the user */ + public static boolean sShowStructure = false; + + /** Whether the drop positions should snap to a regular grid */ + public static boolean sSnapToGrid = false; + + /** + * Whether the grid is edited in "grid mode" where the operations are row/column based + * rather than free-form + */ + public static boolean sGridMode = false; + + /** Constructs a new {@link GridLayoutRule} */ + public GridLayoutRule() { + } + + @Override + public void addLayoutActions(List<MenuAction> actions, final INode parentNode, + final List<? extends INode> children) { + super.addLayoutActions(actions, parentNode, children); + + OrderedChoices orientationAction = MenuAction.createChoices( + ACTION_ORIENTATION, + "Orientation", //$NON-NLS-1$ + null, new PropertyCallback(Collections.singletonList(parentNode), + "Change LinearLayout Orientation", ANDROID_URI, ATTR_ORIENTATION), Arrays + .<String> asList("Set Horizontal Orientation", "Set Vertical Orientation"), + Arrays.<URL> asList(ICON_HORIZONTAL, ICON_VERTICAL), Arrays.<String> asList( + "horizontal", "vertical"), getCurrentOrientation(parentNode), + null /* icon */, -10); + orientationAction.setRadio(true); + actions.add(orientationAction); + + // Gravity and margins + if (children != null && children.size() > 0) { + actions.add(MenuAction.createSeparator(35)); + actions.add(createMarginAction(parentNode, children)); + actions.add(createGravityAction(children, ATTR_LAYOUT_GRAVITY)); + } + + IMenuCallback actionCallback = new IMenuCallback() { + public void action(final MenuAction action, final String valueId, + final Boolean newValue) { + parentNode.editXml("Add/Remove Row/Column", new INodeHandler() { + public void handle(INode n) { + String id = action.getId(); + if (id.equals(ACTION_SHOW_GRID)) { + sShowStructure = !sShowStructure; + // HACK: ToggleButton controls two flags for now - show grid and + // grid mode (handling drags in a grid mode) + sGridMode = !sGridMode; + + mRulesEngine.redraw(); + return; + } else if (id.equals(ACTION_SNAP)) { + sSnapToGrid = !sSnapToGrid; + mRulesEngine.redraw(); + return; + } else if (id.equals(ACTION_DEBUG)) { + sDebugGridLayout = !sDebugGridLayout; + mRulesEngine.layout(); + return; + } + + GridModel grid = new GridModel(mRulesEngine, parentNode); + if (id.equals(ACTION_ADD_ROW)) { + grid.addRow(children); + } else if (id.equals(ACTION_REMOVE_ROW)) { + grid.removeRows(children); + } else if (id.equals(ACTION_ADD_COL)) { + grid.addColumn(children); + } else if (id.equals(ACTION_REMOVE_COL)) { + grid.removeColumns(children); + } + } + + }); + } + }; + + // Add Row and Add Column + actions.add(MenuAction.createSeparator(150)); + actions.add(MenuAction.createAction(ACTION_ADD_COL, "Add Column", null, actionCallback, + ICON_ADD_COL, 160)); + actions.add(MenuAction.createAction(ACTION_ADD_ROW, "Add Row", null, actionCallback, + ICON_ADD_ROW, 165)); + + // Remove Row and Remove Column (if something is selected) + if (children != null && children.size() > 0) { + // TODO: Add "Merge Columns" and "Merge Rows" ? + + actions.add(MenuAction.createAction(ACTION_REMOVE_COL, "Remove Column", null, + actionCallback, ICON_REMOVE_COL, 170)); + actions.add(MenuAction.createAction(ACTION_REMOVE_ROW, "Remove Row", null, + actionCallback, ICON_REMOVE_ROW, 175)); + } + + actions.add(MenuAction.createSeparator(185)); + + actions.add(MenuAction.createToggle(ACTION_SNAP, "Snap to Grid", + sSnapToGrid, actionCallback, ICON_SNAP, 190)); + + actions.add(MenuAction.createToggle(ACTION_SHOW_GRID, "Show Structure", + sShowStructure, actionCallback, ICON_SHOW_GRID, 200)); + + // Temporary: Diagnostics for GridLayout + actions.add(MenuAction.createToggle(ACTION_DEBUG, "Debug", + sDebugGridLayout, actionCallback, null, 210)); + } + + /** + * Returns the orientation attribute value currently used by the node (even if not + * defined, in which case the default horizontal value is returned) + */ + private static String getCurrentOrientation(final INode node) { + String orientation = node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION); + if (orientation == null || orientation.length() == 0) { + orientation = VALUE_HORIZONTAL; + } + return orientation; + } + + @Override + public DropFeedback onDropEnter(INode targetNode, final IDragElement[] elements) { + GridDropHandler userData = new GridDropHandler(this, targetNode); + IFeedbackPainter painter = GridLayoutPainter.createDropFeedbackPainter(this, elements); + return new DropFeedback(userData, painter); + } + + @Override + public DropFeedback onDropMove(INode targetNode, IDragElement[] elements, + DropFeedback feedback, Point p) { + feedback.requestPaint = true; + + GridDropHandler handler = (GridDropHandler) feedback.userData; + handler.computeMatches(feedback, p); + + return feedback; + } + + @Override + public void onDropped(final INode targetNode, final IDragElement[] elements, + DropFeedback feedback, Point p) { + Rect b = targetNode.getBounds(); + if (!b.isValid()) { + return; + } + + GridDropHandler dropHandler = (GridDropHandler) feedback.userData; + if (dropHandler.getRowMatch() == null || dropHandler.getColumnMatch() == null) { + return; + } + + // Collect IDs from dropped elements and remap them to new IDs + // if this is a copy or from a different canvas. + Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements, + feedback.isCopy || !feedback.sameCanvas); + + for (IDragElement element : elements) { + INode newChild; + if (!sGridMode) { + newChild = dropHandler.handleFreeFormDrop(targetNode, element); + } else { + newChild = dropHandler.handleGridModeDrop(targetNode, element); + } + + // Copy all the attributes, modifying them as needed. + addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER); + + addInnerElements(newChild, element, idMap); + } + } + + @Override + public void onRemovingChildren(List<INode> deleted, INode parent) { + super.onRemovingChildren(deleted, parent); + + // 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); + for (INode child : deleted) { + // We don't care about deletion of spacers + if (child.getFqcn().equals(FQCN_SPACE)) { + continue; + } + grid.markDeleted(child); + } + + grid.cleanup(); + } + + @Override + protected void paintResizeFeedback(IGraphics gc, INode node, ResizeState state) { + if (!sGridMode) { + GridModel grid = getGrid(state); + GridLayoutPainter.paintResizeFeedback(gc, state.layout, grid); + } + + if (resizingWidget(state)) { + super.paintResizeFeedback(gc, node, state); + } else { + GridModel grid = getGrid(state); + int startColumn = grid.getColumn(state.bounds.x); + int endColumn = grid.getColumn(state.bounds.x2()); + int columnSpan = endColumn - startColumn + 1; + + int startRow = grid.getRow(state.bounds.y); + int endRow = grid.getRow(state.bounds.y2()); + int rowSpan = endRow - startRow + 1; + + Rect cellBounds = grid.getCellBounds(startRow, startColumn, rowSpan, columnSpan); + gc.useStyle(DrawingStyle.RESIZE_PREVIEW); + gc.drawRect(cellBounds); + } + } + + /** Returns the grid size cached on the given {@link ResizeState} object */ + private GridModel getGrid(ResizeState resizeState) { + GridModel grid = (GridModel) resizeState.clientData; + if (grid == null) { + grid = new GridModel(mRulesEngine, resizeState.layout); + resizeState.clientData = grid; + } + + return grid; + } + + @Override + protected void setNewSizeBounds(ResizeState state, INode node, INode layout, + Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { + + if (resizingWidget(state)) { + super.setNewSizeBounds(state, node, layout, oldBounds, newBounds, horizontalEdge, + verticalEdge); + } else { + Pair<Integer, Integer> spans = computeResizeSpans(state); + int rowSpan = spans.getFirst(); + int columnSpan = spans.getSecond(); + GridModel.setColumnSpanAttribute(node, columnSpan); + GridModel.setRowSpanAttribute(node, rowSpan); + } + } + + @Override + protected String getResizeUpdateMessage(ResizeState state, INode child, INode parent, + Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { + Pair<Integer, Integer> spans = computeResizeSpans(state); + if (resizingWidget(state)) { + String width = state.getWidthAttribute(); + String height = state.getHeightAttribute(); + + // U+00D7: Unicode for multiplication sign + return String.format("%s \u00D7 %s\n(Press Shift to resize row/column spans)", + width, height); + } else { + int rowSpan = spans.getFirst(); + int columnSpan = spans.getSecond(); + return String.format("ColumnSpan=%d, RowSpan=%d\n(Release Shift to resize widget itself)", + columnSpan, rowSpan); + } + } + + /** + * Returns true if we're resizing the widget, and false if we're resizing the cell + * spans + */ + private static boolean resizingWidget(ResizeState state) { + return (state.modifierMask & DropFeedback.MODIFIER2) == 0; + } + + /** + * Computes the new column and row spans as the result of the current resizing + * operation + */ + private Pair<Integer, Integer> computeResizeSpans(ResizeState state) { + GridModel grid = getGrid(state); + + int startColumn = grid.getColumn(state.bounds.x); + int endColumn = grid.getColumn(state.bounds.x2()); + int columnSpan = endColumn - startColumn + 1; + + int startRow = grid.getRow(state.bounds.y); + int endRow = grid.getRow(state.bounds.y2()); + int rowSpan = endRow - startRow + 1; + + return Pair.of(rowSpan, columnSpan); + } + + /** + * Returns the size of the new cell gutter in layout coordinates + * + * @return the size of the new cell gutter in layout coordinates + */ + public int getNewCellSize() { + return mRulesEngine.screenToLayout(NEW_CELL_WIDTH / 2); + } + + @Override + public void paintSelectionFeedback(IGraphics graphics, INode parentNode, + List<? extends INode> childNodes) { + super.paintSelectionFeedback(graphics, parentNode, childNodes); + + if (sShowStructure) { + // TODO: Cache the grid + GridLayoutPainter.paintStructure(DrawingStyle.GUIDELINE_DASHED, + parentNode, graphics, new GridModel(mRulesEngine, parentNode)); + } else if (sDebugGridLayout) { + GridLayoutPainter.paintStructure(DrawingStyle.GRID, + parentNode, graphics, new GridModel(mRulesEngine, parentNode)); + } + + // TBD: Highlight the cells around the selection, and display easy controls + // for for example tweaking the rowspan/colspan of a cell? (but only in grid mode) + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java index 0ef178d..d4ed864 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java @@ -49,10 +49,13 @@ public class LayoutConstants { public static final String LIST_VIEW = "ListView"; //$NON-NLS-1$ public static final String EDIT_TEXT = "EditText"; //$NON-NLS-1$ public static final String GALLERY = "Gallery"; //$NON-NLS-1$ + public static final String GRID_LAYOUT = "GridLayout"; //$NON-NLS-1$ public static final String GRID_VIEW = "GridView"; //$NON-NLS-1$ + public static final String SPINNER = "Spinner"; //$NON-NLS-1$ public static final String SCROLL_VIEW = "ScrollView"; //$NON-NLS-1$ public static final String RADIO_BUTTON = "RadioButton"; //$NON-NLS-1$ public static final String RADIO_GROUP = "RadioGroup"; //$NON-NLS-1$ + public static final String SPACE = "Space"; //$NON-NLS-1$ public static final String EXPANDABLE_LIST_VIEW = "ExpandableListView";//$NON-NLS-1$ public static final String GESTURE_OVERLAY_VIEW = "GestureOverlayView";//$NON-NLS-1$ public static final String HORIZONTAL_SCROLL_VIEW = "HorizontalScrollView"; //$NON-NLS-1$ @@ -97,6 +100,20 @@ public class LayoutConstants { public static final String ATTR_LAYOUT_BELOW = "layout_below"; //$NON-NLS-1$ public static final String ATTR_LAYOUT_ABOVE = "layout_above"; //$NON-NLS-1$ + // GridLayout + public static final String ATTR_ROW_COUNT = "rowCount"; //$NON-NLS-1$ + public static final String ATTR_COLUMN_COUNT = "columnCount"; //$NON-NLS-1$ + public static final String ATTR_USE_DEFAULT_MARGINS = "useDefaultMargins"; //$NON-NLS-1$ + public static final String ATTR_MARGINS_INCLUDED_IN_ALIGNMENT = "marginsIncludedInAlignment"; //$NON-NLS-1$ + + // GridLayout layout params + public static final String ATTR_LAYOUT_ROW = "layout_row"; //$NON-NLS-1$ + public static final String ATTR_LAYOUT_ROW_SPAN = "layout_rowSpan"; //$NON-NLS-1$ + //public static final String ATTR_LAYOUT_ROW_WEIGHT = "layout_rowWeight"; //$NON-NLS-1$ + public static final String ATTR_LAYOUT_COLUMN = "layout_column"; //$NON-NLS-1$ + public static final String ATTR_LAYOUT_COLUMN_SPAN = "layout_columnSpan"; //$NON-NLS-1$ + //public static final String ATTR_LAYOUT_COLUMN_WEIGHT = "layout_columnWeight"; //$NON-NLS-1$ + public static final String ATTR_LAYOUT_Y = "layout_y"; //$NON-NLS-1$ public static final String ATTR_LAYOUT_X = "layout_x"; //$NON-NLS-1$ public static final String ATTR_NAME = "name"; //$NON-NLS-1$ @@ -107,6 +124,17 @@ public class LayoutConstants { public static final String VALUE_FALSE= "false"; //$NON-NLS-1$ public static final String VALUE_N_DP = "%ddp"; //$NON-NLS-1$ public static final String VALUE_ZERO_DP = "0dp"; //$NON-NLS-1$ + public static final String VALUE_ONE_DP = "1dp"; //$NON-NLS-1$ + public static final String VALUE_TOP = "top"; //$NON-NLS-1$ + public static final String VALUE_LEFT = "left"; //$NON-NLS-1$ + public static final String VALUE_RIGHT = "right"; //$NON-NLS-1$ + public static final String VALUE_BOTTOM = "bottom"; //$NON-NLS-1$ + public static final String VALUE_CENTER_VERTICAL = "center_vertical"; //$NON-NLS-1$ + public static final String VALUE_CENTER_HORIZONTAL = "center_horizontal"; //$NON-NLS-1$ + public static final String VALUE_FILL_HORIZONTAL = "fill_horizontal"; //$NON-NLS-1$ + public static final String VALUE_FILL_VERTICAL = "fill_vertical"; //$NON-NLS-1$ + public static final String VALUE_0 = "0"; //$NON-NLS-1$ + public static final String VALUE_1 = "1"; //$NON-NLS-1$ // Gravity values. These have the GRAVITY_ prefix in front of value because we already // have VALUE_CENTER_HORIZONTAL defined for layouts, and its definition conflicts @@ -178,6 +206,12 @@ public class LayoutConstants { /** The fully qualified class name of a RadioButton view */ public static final String FQCN_RADIO_BUTTON = "android.widget.RadioButton"; //$NON-NLS-1$ + /** The fully qualified class name of a ToggleButton view */ + public static final String FQCN_TOGGLE_BUTTON = "android.widget.ToggleButton"; //$NON-NLS-1$ + + /** The fully qualified class name of a Spinner view */ + public static final String FQCN_SPINNER = "android.widget.Spinner"; //$NON-NLS-1$ + /** The fully qualified class name of an AdapterView */ public static final String FQCN_ADAPTER_VIEW = "android.widget.AdapterView"; //$NON-NLS-1$ @@ -199,6 +233,9 @@ public class LayoutConstants { /** The fully qualified class name of a RadioGroup */ public static final String FQCN_RADIO_GROUP = "android.widgets.RadioGroup"; //$NON-NLS-1$ + /** The fully qualified class name of a Space */ + public static final String FQCN_SPACE = "android.widget.Space"; //$NON-NLS-1$ + public static final String ATTR_SRC = "src"; //$NON-NLS-1$ // like fill_parent for API 8 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java index 00b8e53..13f06ad 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java @@ -24,6 +24,7 @@ import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WEIGHT; 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.ATTR_WEIGHT_SUM; +import static com.android.ide.common.layout.LayoutConstants.VALUE_1; import static com.android.ide.common.layout.LayoutConstants.VALUE_HORIZONTAL; import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL; import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; @@ -56,6 +57,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; /** @@ -578,7 +580,7 @@ public class LinearLayoutRule extends BaseLayoutRule { // In a horizontal layout, make views that would fill horizontally in a // vertical layout have a non-zero weight instead. This will make the item // fill but only enough to allow other views to be shown as well. - node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, "1"); //$NON-NLS-1$ + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, VALUE_1); } if (fill.fillVertically(vertical)) { node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, fillParent); @@ -1006,7 +1008,7 @@ public class LinearLayoutRule extends BaseLayoutRule { if (resizeState.useWeight) { String weight = formatFloatAttribute(resizeState.mWeight); - String dimension = String.format("layout weight %1$s", weight); + String dimension = String.format("weight %1$s", weight); String width; String height; @@ -1018,8 +1020,14 @@ public class LinearLayoutRule extends BaseLayoutRule { height = resizeState.getHeightAttribute(); } - // U+00D7: Unicode for multiplication sign - return String.format("Resize to %s \u00D7 %s", width, height); + if (horizontalEdge == null) { + return width; + } else if (verticalEdge == null) { + return height; + } else { + // U+00D7: Unicode for multiplication sign + return String.format("%s \u00D7 %s", width, height); + } } else { return super.getResizeUpdateMessage(state, child, parent, newBounds, horizontalEdge, verticalEdge); @@ -1078,7 +1086,9 @@ public class LinearLayoutRule extends BaseLayoutRule { @VisibleForTesting static String formatFloatAttribute(float value) { if (value != (int) value) { - return String.format("%.2f", value); //$NON-NLS-1$ + // Run String.format without a locale, because we don't want locale-specific + // conversions here like separating the decimal part with a comma instead of a dot! + return String.format((Locale) null, "%.2f", value); //$NON-NLS-1$ } else { return Integer.toString((int) value); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java index c332649..d53436f 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java @@ -18,6 +18,7 @@ package com.android.ide.common.layout; import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; import static com.android.ide.common.layout.LayoutConstants.ATTR_GRAVITY; +import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ABOVE; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BOTTOM; @@ -36,6 +37,8 @@ import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_V import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF; +import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX; +import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE; import com.android.ide.common.api.DropFeedback; @@ -43,6 +46,7 @@ import com.android.ide.common.api.IDragElement; 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.INode.IAttribute; import com.android.ide.common.api.INodeHandler; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.InsertType; @@ -60,8 +64,10 @@ import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * An {@link IViewRule} for android.widget.RelativeLayout and all its derived @@ -130,11 +136,11 @@ public class RelativeLayoutRule extends BaseLayoutRule { super.paintSelectionFeedback(graphics, parentNode, childNodes); boolean showDependents = true; - if (RelativeLayoutRule.sShowStructure) { + if (sShowStructure) { childNodes = Arrays.asList(parentNode.getChildren()); // Avoid painting twice - both as incoming and outgoing showDependents = false; - } else if (!RelativeLayoutRule.sShowConstraints) { + } else if (!sShowConstraints) { return; } @@ -238,6 +244,44 @@ public class RelativeLayoutRule extends BaseLayoutRule { //} } + @Override + public void onRemovingChildren(List<INode> deleted, INode parent) { + super.onRemovingChildren(deleted, parent); + + // Remove any attachments pointing to the deleted nodes. + + // Produce set of attribute values that we want to delete if + // present in a layout attribute + Set<String> removeValues = new HashSet<String>(deleted.size() * 2); + for (INode node : deleted) { + String id = node.getStringAttr(ANDROID_URI, ATTR_ID); + if (id != null) { + removeValues.add(id); + if (id.startsWith(NEW_ID_PREFIX)) { + removeValues.add(ID_PREFIX + stripIdPrefix(id)); + } else { + removeValues.add(NEW_ID_PREFIX + stripIdPrefix(id)); + } + } + } + + for (INode child : parent.getChildren()) { + if (deleted.contains(child)) { + continue; + } + for (IAttribute attribute : child.getLiveAttributes()) { + if (attribute.getName().startsWith(ATTR_LAYOUT_PREFIX) && + ANDROID_URI.equals(attribute.getUri())) { + String value = attribute.getValue(); + if (removeValues.contains(value)) { + // Unset this reference to a deleted widget. + child.setAttribute(ANDROID_URI, attribute.getName(), null); + } + } + } + } + } + // ==== Resize Support ==== @Override diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ResizeState.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ResizeState.java index 11f3ec6..d67a77c 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ResizeState.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ResizeState.java @@ -71,6 +71,12 @@ class ResizeState { /** Whether the user has snapped to the match_parent height */ public boolean fillHeight; + /** Custom field for use by subclasses */ + public Object clientData; + + /** Keyboard mask */ + public int modifierMask; + /** * Constructs a new {@link ResizeState} * diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/addcol.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/addcol.png Binary files differnew file mode 100644 index 0000000..21391ef --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/addcol.png 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 new file mode 100644 index 0000000..75e69d9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java @@ -0,0 +1,713 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.common.layout.grid; + +import static com.android.ide.common.layout.GridLayoutRule.GRID_SIZE; +import static com.android.ide.common.layout.GridLayoutRule.MARGIN_SIZE; +import static com.android.ide.common.layout.GridLayoutRule.MAX_CELL_DIFFERENCE; +import static com.android.ide.common.layout.GridLayoutRule.SHORT_GAP_DP; +import static com.android.ide.common.layout.LayoutConstants.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_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_1; +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; + +import com.android.ide.common.api.DropFeedback; +import com.android.ide.common.api.IDragElement; +import com.android.ide.common.api.INode; +import com.android.ide.common.api.IViewMetadata; +import com.android.ide.common.api.Margins; +import com.android.ide.common.api.Point; +import com.android.ide.common.api.Rect; +import com.android.ide.common.api.SegmentType; +import com.android.ide.common.layout.BaseLayoutRule; +import com.android.ide.common.layout.GridLayoutRule; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * The {@link GridDropHandler} handles drag and drop operations into and within a + * GridLayout, computing guidelines, handling drops to edit the grid model, and so on. + */ +public class GridDropHandler { + private final GridModel mGrid; + private final GridLayoutRule mRule; + private GridMatch mColumnMatch; + private GridMatch mRowMatch; + + /** + * Creates a new {@link GridDropHandler} for + * @param gridLayoutRule the corresponding {@link GridLayoutRule} + * @param layout the GridLayout node + */ + public GridDropHandler(GridLayoutRule gridLayoutRule, INode layout) { + mRule = gridLayoutRule; + mGrid = new GridModel(mRule.getRulesEngine(), layout); + } + + /** + * Computes the best horizontal and vertical matches for a drag to the given position. + * + * @param feedback a {@link DropFeedback} object containing drag state like the drag + * bounds and the drag baseline + * @param p the mouse position + */ + public void computeMatches(DropFeedback feedback, Point p) { + mRowMatch = mColumnMatch = null; + + Rect bounds = mGrid.layout.getBounds(); + int x1 = p.x; + int y1 = p.y; + + if (!GridLayoutRule.sGridMode) { + Rect dragBounds = feedback.dragBounds; + if (dragBounds != null) { + // Sometimes the items are centered under the mouse so + // offset by the top left corner distance + x1 += dragBounds.x; + y1 += dragBounds.y; + } + + int w = dragBounds != null ? dragBounds.w : 0; + int h = dragBounds != null ? dragBounds.h : 0; + int x2 = x1 + w; + int y2 = y1 + h; + + if (x2 < bounds.x || y2 < bounds.y || x1 > bounds.x2() || y1 > bounds.y2()) { + return; + } + + List<GridMatch> columnMatches = new ArrayList<GridMatch>(); + List<GridMatch> rowMatches = new ArrayList<GridMatch>(); + int max = BaseLayoutRule.getMaxMatchDistance(); + + // Column matches: + addLeftSideMatch(x1, columnMatches, max); + addRightSideMatch(x2, columnMatches, max); + addCenterColumnMatch(bounds, x1, y1, x2, y2, columnMatches, max); + + // Row matches: + int row = mGrid.getClosestRow(y1); + int rowY = mGrid.getRowY(row); + addTopMatch(y1, rowMatches, max, row, rowY); + addBaselineMatch(feedback.dragBaseline, y1, rowMatches, max, row, rowY); + addBottomMatch(y2, rowMatches, max); + + // Look for gap-matches: Predefined spacing between widgets. + // TODO: Make this use metadata for predefined spacing between + // pairs of types of components. For example, buttons have certain + // inserts in their 9-patch files (depending on the theme) that should + // be considered and subtracted from the overall proposed distance! + addColumnGapMatch(bounds, x1, x2, columnMatches, max); + addRowGapMatch(bounds, y1, y2, rowMatches, max); + + // Fallback: Split existing cell. Also do snap-to-grid. + if (GridLayoutRule.sSnapToGrid) { + x1 = ((x1 - MARGIN_SIZE - bounds.x) / GRID_SIZE) * GRID_SIZE + + MARGIN_SIZE + bounds.x; + y1 = ((y1 - MARGIN_SIZE - bounds.y) / GRID_SIZE) * GRID_SIZE + + MARGIN_SIZE + bounds.y; + x2 = x1 + w; + y2 = y1 + h; + } + + + if (columnMatches.size() == 0) { + // Split the current cell since we have no matches + // TODO: Decide whether it should be gravity left or right... + columnMatches.add(new GridMatch(SegmentType.LEFT, 0, x1, mGrid.getColumn(x1), + true /* createCell */, UNDEFINED)); + } + if (rowMatches.size() == 0) { + rowMatches.add(new GridMatch(SegmentType.TOP, 0, y1, mGrid.getRow(y1), + true /* createCell */, UNDEFINED)); + } + + // Pick best matches + Collections.sort(rowMatches); + Collections.sort(columnMatches); + + mColumnMatch = columnMatches.size() > 0 ? columnMatches.get(0) : null; + mRowMatch = rowMatches.size() > 0 ? rowMatches.get(0) : null; + + String columnDescription = mColumnMatch != null ? mColumnMatch.getDisplayName() : null; + String rowDescription = mRowMatch != null ? mRowMatch.getDisplayName() : null; + if (columnDescription != null) { + if (rowDescription != null) { + feedback.tooltip = columnDescription + '\n' + rowDescription; + } else { + feedback.tooltip = columnDescription; + } + } else if (rowDescription != null) { + feedback.tooltip = rowDescription; + } else { + feedback.tooltip = null; + } + + feedback.invalidTarget = mColumnMatch == null || mRowMatch == null; + } else { + // Find which cell we're inside. + + // TODO: Find out where within the cell we are, and offer to tweak the gravity + // based on the position. + int column = mGrid.getColumn(x1); + int row = mGrid.getRow(y1); + + int leftDistance = mGrid.getColumnDistance(column, x1); + int rightDistance = mGrid.getColumnDistance(column + 1, x1); + int topDistance = mGrid.getRowDistance(row, y1); + int bottomDistance = mGrid.getRowDistance(row + 1, y1); + + int SLOP = 2; + int radius = mRule.getNewCellSize(); + if (rightDistance < radius + SLOP) { + column++; + leftDistance = rightDistance; + } + if (bottomDistance < radius + SLOP) { + row++; + topDistance = bottomDistance; + } + + boolean matchLeft = leftDistance < radius + SLOP; + boolean matchTop = topDistance < radius + SLOP; + + mColumnMatch = new GridMatch(SegmentType.LEFT, 0, x1, column, matchLeft, 0); + mRowMatch = new GridMatch(SegmentType.TOP, 0, y1, row, matchTop, 0); + } + } + + /** + * Adds a match to align the left edge with some other edge. + */ + private void addLeftSideMatch(int x1, List<GridMatch> columnMatches, int max) { + int column = mGrid.getClosestColumn(x1); + int columnX = mGrid.getColumnX(column); + int distance = abs(columnX - x1); + if (distance <= max) { + columnMatches.add(new GridMatch(SegmentType.LEFT, distance, columnX, column, + false, UNDEFINED)); + } + } + + /** + * Adds a match to align the right edge with some other edge. + */ + private void addRightSideMatch(int x2, List<GridMatch> columnMatches, int max) { + // TODO: Only match the right hand side if the drag bounds fit fully within the + // cell! Ditto for match below. + int columnRight = mGrid.getClosestColumn(x2); + int rightDistance = mGrid.getColumnDistance(columnRight, x2); + if (rightDistance < max) { + columnMatches.add(new GridMatch(SegmentType.RIGHT, rightDistance, + mGrid.getColumnX(columnRight), columnRight, false, UNDEFINED)); + } + } + + /** + * Adds a horizontal match with the center axis of the GridLayout + */ + private void addCenterColumnMatch(Rect bounds, int x1, int y1, int x2, int y2, + List<GridMatch> columnMatches, int max) { + Collection<INode> intersectsRow = mGrid.getIntersectsRow(y1, y2); + if (intersectsRow.size() == 0) { + // Offer centering on this row since there isn't anything there + int matchedLine = bounds.centerX(); + int distance = abs((x1 + x2) / 2 - matchedLine); + if (distance <= 2 * max) { + boolean createCell = false; // always just put in column 0 + columnMatches.add(new GridMatch(SegmentType.CENTER_HORIZONTAL, distance, + matchedLine, 0 /* column */, createCell, UNDEFINED)); + } + } + } + + /** + * Adds a match to align the top edge with some other edge. + */ + private void addTopMatch(int y1, List<GridMatch> rowMatches, int max, int row, int rowY) { + int distance = mGrid.getRowDistance(row, y1); + if (distance <= max) { + rowMatches.add(new GridMatch(SegmentType.TOP, distance, rowY, row, false, + UNDEFINED)); + } + } + + /** + * Adds a match to align the bottom edge with some other edge. + */ + private void addBottomMatch(int y2, List<GridMatch> rowMatches, int max) { + int rowBottom = mGrid.getClosestRow(y2); + int distance = mGrid.getRowDistance(rowBottom, y2); + if (distance < max) { + int rowY = mGrid.getRowY(rowBottom); + rowMatches.add(new GridMatch(SegmentType.BOTTOM, distance, rowY, + rowBottom, false, UNDEFINED)); + } + } + + /** + * Adds a baseline match, if applicable. + */ + private void addBaselineMatch(int dragBaseline, int y1, List<GridMatch> rowMatches, int max, + int row, int rowY) { + int dragBaselineY = y1 + dragBaseline; + int rowBaseline = mGrid.getBaseline(row); + if (rowBaseline != -1) { + int rowBaselineY = rowY + rowBaseline; + int distance = abs(dragBaselineY - rowBaselineY); + if (distance < max) { + rowMatches.add(new GridMatch(SegmentType.BASELINE, distance, rowBaselineY, row, + false, UNDEFINED)); + } + } + } + + /** + * Computes a horizontal "gap" match - a preferred distance from the nearest edge, + * including margin edges + */ + private void addColumnGapMatch(Rect bounds, int x1, int x2, List<GridMatch> columnMatches, + int max) { + if (x1 < bounds.x + MARGIN_SIZE + max) { + int matchedLine = bounds.x + MARGIN_SIZE; + int distance = abs(matchedLine - x1); + if (distance <= max) { + boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; + columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine, + 0, createCell, MARGIN_SIZE)); + } + } else if (x2 > bounds.x2() - MARGIN_SIZE - max) { + int matchedLine = bounds.x2() - MARGIN_SIZE; + int distance = abs(matchedLine - x2); + if (distance <= max) { + // This does not yet work properly; we need to use columnWeights to achieve this + //boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; + //columnMatches.add(new GridMatch(SegmentType.RIGHT, distance, matchedLine, + // mGrid.actualColumnCount - 1, createCell, MARGIN_SIZE)); + } + } else { + int columnRight = mGrid.getColumn(x1 - SHORT_GAP_DP); + int columnX = mGrid.getColumnMaxX(columnRight); + int matchedLine = columnX + SHORT_GAP_DP; + int distance = abs(matchedLine - x1); + if (distance <= max) { + boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; + columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine, + columnRight, createCell, SHORT_GAP_DP)); + } + + // Add a column directly adjacent (no gap) + columnRight = mGrid.getColumn(x1); + columnX = mGrid.getColumnMaxX(columnRight); + matchedLine = columnX; + distance = abs(matchedLine - x1); + if (distance <= max) { + boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine; + columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine, + columnRight, createCell, 0)); + } + } + } + + /** + * Computes a vertical "gap" match - a preferred distance from the nearest edge, + * including margin edges + */ + private void addRowGapMatch(Rect bounds, int y1, int y2, List<GridMatch> rowMatches, int max) { + if (y1 < bounds.y + MARGIN_SIZE + max) { + int matchedLine = bounds.y + MARGIN_SIZE; + int distance = abs(matchedLine - y1); + if (distance <= max) { + boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; + rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine, + 0, createCell, MARGIN_SIZE)); + } + } else if (y2 > bounds.y2() - MARGIN_SIZE - max) { + int matchedLine = bounds.y2() - MARGIN_SIZE; + int distance = abs(matchedLine - y2); + if (distance <= max) { + // This does not yet work properly; we need to use columnWeights to achieve this + //boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; + //rowMatches.add(new GridMatch(SegmentType.BOTTOM, distance, matchedLine, + // mGrid.actualRowCount - 1, createCell, MARGIN_SIZE)); + } + } else { + int rowBottom = mGrid.getRow(y1 - SHORT_GAP_DP); + int rowY = mGrid.getRowMaxY(rowBottom); + int matchedLine = rowY + SHORT_GAP_DP; + int distance = abs(matchedLine - y1); + if (distance <= max) { + boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; + rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine, + rowBottom, createCell, SHORT_GAP_DP)); + } + + // Add a row directly adjacent (no gap) + rowBottom = mGrid.getRow(y1); + rowY = mGrid.getRowMaxY(rowBottom); + matchedLine = rowY; + distance = abs(matchedLine - y1); + if (distance <= max) { + boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine; + rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine, + rowBottom, createCell, 0)); + } + + } + } + + /** + * Called when a node is dropped in free-form mode. This will insert the dragged + * element into the grid and returns the newly created node. + * + * @param targetNode the GridLayout node + * @param element the dragged element + * @return the newly created {@link INode} + */ + public INode handleFreeFormDrop(INode targetNode, IDragElement element) { + assert mRowMatch != null; + assert mColumnMatch != null; + + String fqcn = element.getFqcn(); + + INode newChild = null; + + Rect bounds = element.getBounds(); + int row = mRowMatch.cellIndex; + int column = mColumnMatch.cellIndex; + + if (targetNode.getChildren().length == 0) { + // + // Set up the initial structure: + // + // + // Fixed Fixed + // Size Size + // Column Expanding Column Column + // +-----+-------------------------------+-----+ + // | | | | + // | 0,0 | 0,1 | 0,2 | Fixed Size Row + // | | | | + // +-----+-------------------------------+-----+ + // | | | | + // | | | | + // | | | | + // | 1,0 | 1,1 | 1,2 | Expanding Row + // | | | | + // | | | | + // | | | | + // +-----+-------------------------------+-----+ + // | | | | + // | 2,0 | 2,1 | 2,2 | Fixed Size Row + // | | | | + // +-----+-------------------------------+-----+ + // + // This is implemented in GridLayout by the following grid, where + // SC1 has columnWeight=1 and SR1 has rowWeight=1. + // (SC=Space for Column, SR=Space for Row) + // + // +------+-------------------------------+------+ + // | | | | + // | SCR0 | SC1 | SC2 | + // | | | | + // +------+-------------------------------+------+ + // | | | | + // | | | | + // | | | | + // | SR1 | | | + // | | | | + // | | | | + // | | | | + // +------+-------------------------------+------+ + // | | | | + // | SR2 | | | + // | | | | + // +------+-------------------------------+------+ + // + // Note that when we split columns and rows here, if splitting the expanding + // row or column then the row or column weight should be moved to the right or + // bottom half! + + + int columnX = mGrid.getColumnX(column); + int rowY = mGrid.getRowY(row); + + targetNode.setAttribute(ANDROID_URI, ATTR_COLUMN_COUNT, VALUE_1); + //targetNode.setAttribute(ANDROID_URI, ATTR_COLUMN_COUNT, "3"); + //INode scr0 = addSpacer(targetNode, -1, 0, 0, 1, 1); + //INode sc1 = addSpacer(targetNode, -1, 0, 1, 0, 0); + //INode sc2 = addSpacer(targetNode, -1, 0, 2, 1, 0); + //INode sr1 = addSpacer(targetNode, -1, 1, 0, 0, 0); + //INode sr2 = addSpacer(targetNode, -1, 2, 0, 0, 1); + //sc1.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN_WEIGHT, VALUE_1); + //sr1.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW_WEIGHT, VALUE_1); + //sc1.setAttribute(ANDROID_URI, ATTR_LAYOUT_GRAVITY, VALUE_FILL_HORIZONTAL); + //sr1.setAttribute(ANDROID_URI, ATTR_LAYOUT_GRAVITY, VALUE_FILL_VERTICAL); + + mGrid.loadFromXml(); + column = mGrid.getColumn(columnX); + row = mGrid.getRow(rowY); + } + + int startX, endX; + if (mColumnMatch.type == SegmentType.RIGHT) { + endX = mColumnMatch.matchedLine - 1; + startX = endX - bounds.w; + column = mGrid.getColumn(startX); + } else { + startX = mColumnMatch.matchedLine; // TODO: What happens on type=RIGHT? + endX = startX + bounds.w; + } + int startY, endY; + if (mRowMatch.type == SegmentType.BOTTOM) { + endY = mRowMatch.matchedLine - 1; + startY = endY - bounds.h; + row = mGrid.getRow(startY); + } else if (mRowMatch.type == SegmentType.BASELINE) { + // TODO: The rowSpan should always be 1 for baseline alignments, since + // otherwise the alignment won't work! + startY = endY = mRowMatch.matchedLine; + } else { + startY = mRowMatch.matchedLine; + endY = startY + bounds.h; + } + int endColumn = mGrid.getColumn(endX); + int endRow = mGrid.getRow(endY); + int columnSpan = endColumn - column + 1; + int rowSpan = endRow - row + 1; + + // Make sure my math was right: + if (mRowMatch.type == SegmentType.BASELINE) { + assert rowSpan == 1 : rowSpan; + } + + // If the item almost fits into the row (at most N % bigger) then just enlarge + // the row; don't add a rowspan since that will defeat baseline alignment etc + if (!mRowMatch.createCell && bounds.h <= MAX_CELL_DIFFERENCE * mGrid.getRowHeight( + mRowMatch.type == SegmentType.BOTTOM ? endRow : row, 1)) { + if (mRowMatch.type == SegmentType.BOTTOM) { + row += rowSpan - 1; + } + rowSpan = 1; + } + if (!mColumnMatch.createCell && bounds.w <= MAX_CELL_DIFFERENCE * mGrid.getColumnWidth( + mColumnMatch.type == SegmentType.RIGHT ? endColumn : column, 1)) { + if (mColumnMatch.type == SegmentType.RIGHT) { + column += columnSpan - 1; + } + columnSpan = 1; + } + + if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) { + column = 0; + columnSpan = mGrid.actualColumnCount; + } + + // Temporary: Ensure we don't get in trouble with implicit positions + mGrid.applyPositionAttributes(); + + // Split cells to make a new column + if (mColumnMatch.createCell) { + int columnWidthPx = mGrid.getColumnDistance(column, mColumnMatch.matchedLine); + //assert columnWidthPx == columnMatch.distance; // TBD? IF so simplify + int columnWidthDp = mRule.getRulesEngine().pxToDp(columnWidthPx); + + int maxX = mGrid.getColumnMaxX(column); + boolean insertMarginColumn = false; + if (mColumnMatch.margin == 0) { + columnWidthDp = 0; + } else if (mColumnMatch.margin != UNDEFINED) { + int distance = abs(mColumnMatch.matchedLine - (maxX + mColumnMatch.margin)); + insertMarginColumn = column > 0 && distance < 2; + if (insertMarginColumn) { + int margin = mColumnMatch.margin; + IViewMetadata metadata = mRule.getRulesEngine().getMetadata(element.getFqcn()); + if (metadata != null) { + Margins insets = metadata.getInsets(); + if (insets != null) { + // TODO: + // Consider left or right side attachment + // TODO: Also consider inset of element on cell to the left + margin -= insets.left; + } + } + + columnWidthDp = mRule.getRulesEngine().pxToDp(margin); + } + } + + column++; + mGrid.splitColumn(column, insertMarginColumn, columnWidthDp, mColumnMatch.matchedLine); + 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 + if (mRowMatch.createCell) { + int rowHeightPx = mGrid.getRowDistance(row, mRowMatch.matchedLine); + //assert rowHeightPx == rowMatch.distance; // TBD? If so simplify + int rowHeightDp = mRule.getRulesEngine().pxToDp(rowHeightPx); + + int maxY = mGrid.getRowMaxY(row); + boolean insertMarginRow = false; + if (mRowMatch.margin == 0) { + rowHeightDp = 0; + } else if (mRowMatch.margin != UNDEFINED) { + int distance = abs(mRowMatch.matchedLine - (maxY + mRowMatch.margin)); + insertMarginRow = row > 0 && distance < 2; + if (insertMarginRow) { + int margin = mRowMatch.margin; + IViewMetadata metadata = mRule.getRulesEngine().getMetadata(element.getFqcn()); + if (metadata != null) { + Margins insets = metadata.getInsets(); + if (insets != null) { + // TODO: + // Consider left or right side attachment + // TODO: Also consider inset of element on cell to the left + margin -= insets.top; + } + } + + rowHeightDp = mRule.getRulesEngine().pxToDp(margin); + } + } + + row++; + mGrid.splitRow(row, insertMarginRow, rowHeightDp, mRowMatch.matchedLine); + if (insertMarginRow) { + row++; + } + mGrid.loadFromXml(); + } + + // Figure out where to insert the new child + + int index = mGrid.getInsertIndex(row, column); + if (index == -1) { + // Couldn't find a later place to insert + newChild = targetNode.appendChild(fqcn); + } else { + GridModel.ViewData next = mGrid.getView(index); + + newChild = targetNode.insertChildAt(fqcn, index); + + // Must also apply positions to the following child to ensure + // that the new child doesn't affect the implicit numbering! + // TODO: We can later check whether the implied number is equal to + // what it already is such that we don't need this + next.applyPositionAttributes(); + } + + // Set the cell position of the new widget + if (mColumnMatch.type == SegmentType.RIGHT) { + newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_GRAVITY, VALUE_RIGHT); + } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) { + newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_GRAVITY, VALUE_CENTER_HORIZONTAL); + } + newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN, Integer.toString(column)); + if (mRowMatch.type == SegmentType.BOTTOM) { + String value = VALUE_BOTTOM; + if (mColumnMatch.type == SegmentType.RIGHT) { + value = value + '|' + VALUE_RIGHT; + } + newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_GRAVITY, value); + } + newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, Integer.toString(row)); + + // Apply spans to ensure that the widget can fit without pushing columns + if (columnSpan > 1) { + newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN_SPAN, + Integer.toString(columnSpan)); + } + if (rowSpan > 1) { + newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW_SPAN, Integer.toString(rowSpan)); + } + + return newChild; + } + + /** + * Called when a drop is completed and we're in grid-editing mode. This will insert + * the dragged element into the target cell. + * + * @param targetNode the GridLayout node + * @param element the dragged element + * @return the newly created node + */ + public INode handleGridModeDrop(INode targetNode, IDragElement element) { + String fqcn = element.getFqcn(); + INode newChild = targetNode.appendChild(fqcn); + + if (mColumnMatch.createCell) { + mGrid.addColumn(mColumnMatch.cellIndex, + newChild, UNDEFINED, false, UNDEFINED, UNDEFINED); + } + if (mRowMatch.createCell) { + mGrid.loadFromXml(); + mGrid.addRow(mRowMatch.cellIndex, newChild, UNDEFINED, false, UNDEFINED, UNDEFINED); + } + + newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN, + Integer.toString(mColumnMatch.cellIndex)); + newChild.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, + Integer.toString(mRowMatch.cellIndex)); + + return newChild; + } + + /** + * Returns the best horizontal match + * + * @return the best horizontal match, or null if there is no match + */ + public GridMatch getColumnMatch() { + return mColumnMatch; + } + + /** + * Returns the best vertical match + * + * @return the best vertical match, or null if there is no match + */ + public GridMatch getRowMatch() { + return mRowMatch; + } + + /** + * Returns the grid used by the drop handler + * + * @return the grid used by the drop handler, never null + */ + public GridModel getGrid() { + return mGrid; + } +}
\ No newline at end of file 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 new file mode 100644 index 0000000..cf4e21b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridLayoutPainter.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.common.layout.grid; + +import static com.android.ide.common.layout.GridLayoutRule.GRID_SIZE; +import static com.android.ide.common.layout.GridLayoutRule.MARGIN_SIZE; +import static com.android.ide.common.layout.grid.GridModel.UNDEFINED; + +import com.android.ide.common.api.DrawingStyle; +import com.android.ide.common.api.DropFeedback; +import com.android.ide.common.api.IDragElement; +import com.android.ide.common.api.IFeedbackPainter; +import com.android.ide.common.api.IGraphics; +import com.android.ide.common.api.INode; +import com.android.ide.common.api.Rect; +import com.android.ide.common.api.SegmentType; +import com.android.ide.common.layout.GridLayoutRule; + +/** + * Painter which paints feedback during drag, drop and resizing operations, as well as + * static selection feedback + */ +public class GridLayoutPainter { + + /** + * Creates a painter for drop feedback + * + * @param rule the corresponding {@link GridLayoutRule} + * @param elements the dragged elements + * @return a {@link IFeedbackPainter} which can paint the drop feedback + */ + public static IFeedbackPainter createDropFeedbackPainter(GridLayoutRule rule, + IDragElement[] elements) { + return new DropFeedbackPainter(rule, elements); + } + + /** + * Paints the structure (the grid model) of the given GridLayout. + * + * @param style the drawing style to use to paint the structure lines + * @param layout the grid layout node + * @param gc the graphics context to paint into + * @param grid the grid model to be visualized + */ + public static void paintStructure(DrawingStyle style, INode layout, IGraphics gc, + GridModel grid) { + Rect b = layout.getBounds(); + + gc.useStyle(style); + for (int row = 0; row < grid.actualRowCount; row++) { + int y = grid.getRowY(row); + gc.drawLine(b.x, y, b.x2(), y); + } + for (int column = 0; column < grid.actualColumnCount; column++) { + int x = grid.getColumnX(column); + gc.drawLine(x, b.y, x, b.y2()); + } + } + + /** + * Paints a regular grid according to the {@link GridLayoutRule#GRID_SIZE} and + * {@link GridLayoutRule#MARGIN_SIZE} dimensions. These are the same lines that + * snap-to-grid will align with. + * + * @param layout the GridLayout node + * @param gc the graphics context to paint the grid into + */ + public static void paintGrid(INode layout, IGraphics gc) { + Rect b = layout.getBounds(); + + int oldAlpha = gc.getAlpha(); + gc.useStyle(DrawingStyle.GUIDELINE); + gc.setAlpha(128); + + int y1 = b.y + MARGIN_SIZE; + int y2 = b.y2() - MARGIN_SIZE; + for (int y = y1; y < y2; y += GRID_SIZE) { + int x1 = b.x + MARGIN_SIZE; + int x2 = b.x2() - MARGIN_SIZE; + for (int x = x1; x < x2; x += GRID_SIZE) { + gc.drawPoint(x, y); + } + } + gc.setAlpha(oldAlpha); + } + + /** + * Paint resizing feedback (which currently paints the grid model faintly.) + * + * @param gc the graphics context + * @param layout the GridLayout + * @param grid the grid model + */ + public static void paintResizeFeedback(IGraphics gc, INode layout, GridModel grid) { + paintStructure(DrawingStyle.GRID, layout, gc, grid); + } + + /** + * A painter which can paint the drop feedback for elements being dragged into or + * within a GridLayout. + */ + private static class DropFeedbackPainter implements IFeedbackPainter { + private final GridLayoutRule mRule; + private final IDragElement[] mElements; + + /** Constructs a new {@link GridLayoutPainter} bound to the given {@link GridLayoutRule} + * @param rule the corresponding rule + * @param elements the elements to draw */ + public DropFeedbackPainter(GridLayoutRule rule, IDragElement[] elements) { + mRule = rule; + mElements = elements; + } + + // Implements IFeedbackPainter + public void paint(IGraphics gc, INode node, DropFeedback feedback) { + Rect b = node.getBounds(); + if (!b.isValid()) { + return; + } + + // Highlight the receiver + gc.useStyle(DrawingStyle.DROP_RECIPIENT); + gc.drawRect(b); + GridDropHandler data = (GridDropHandler) feedback.userData; + + if (!GridLayoutRule.sGridMode) { + paintFreeFormDropFeedback(gc, node, feedback, b, data); + } else { + paintGridModeDropFeedback(gc, b, data); + } + } + + /** + * Paints the drag feedback for a free-form mode drag + */ + private void paintFreeFormDropFeedback(IGraphics gc, INode node, DropFeedback feedback, + Rect b, GridDropHandler data) { + GridModel grid = data.getGrid(); + if (GridLayoutRule.sSnapToGrid) { + GridLayoutPainter.paintGrid(node, gc); + } + GridLayoutPainter.paintStructure(DrawingStyle.GRID, node, gc, grid); + + GridMatch rowMatch = data.getRowMatch(); + GridMatch columnMatch = data.getColumnMatch(); + + if (rowMatch == null || columnMatch == null) { + return; + } + + IDragElement first = mElements[0]; + Rect dragBounds = first.getBounds(); + int offsetX = 0; + int offsetY = 0; + if (rowMatch.type == SegmentType.BOTTOM) { + offsetY -= dragBounds.h; + } else if (rowMatch.type == SegmentType.BASELINE) { + offsetY -= feedback.dragBaseline; + } + if (columnMatch.type == SegmentType.RIGHT) { + offsetX -= dragBounds.w; + } else if (columnMatch.type == SegmentType.CENTER_HORIZONTAL) { + offsetX -= dragBounds.centerX(); + } + + // Draw guidelines for matches + int y = rowMatch.matchedLine; + int x = columnMatch.matchedLine; + Rect bounds = first.getBounds(); + + // Draw margin + if (rowMatch.margin != UNDEFINED && rowMatch.margin > 0) { + gc.useStyle(DrawingStyle.DISTANCE); + int centerX = bounds.w / 2 + offsetX + x; + int y1; + int y2; + if (rowMatch.type == SegmentType.TOP) { + y1 = offsetY + y - 1; + y2 = rowMatch.matchedLine - rowMatch.margin; + } else { + assert rowMatch.type == SegmentType.BOTTOM; + y1 = bounds.h + offsetY + y - 1; + y2 = rowMatch.matchedLine + rowMatch.margin; + } + gc.drawLine(b.x, y1, b.x2(), y1); + gc.drawLine(b.x, y2, b.x2(), y2); + gc.drawString(Integer.toString(rowMatch.margin), + centerX - 3, y1 + (y2 - y1 - 16) / 2); + } else { + gc.useStyle(rowMatch.margin == 0 ? DrawingStyle.DISTANCE + : rowMatch.createCell ? DrawingStyle.GUIDELINE_DASHED + : DrawingStyle.GUIDELINE); + gc.drawLine(b.x, y, b.x2(), y ); + } + + if (columnMatch.margin != UNDEFINED && columnMatch.margin > 0) { + gc.useStyle(DrawingStyle.DISTANCE); + int centerY = bounds.h / 2 + offsetY + y; + int x1; + int x2; + if (columnMatch.type == SegmentType.LEFT) { + x1 = offsetX + x - 1; + x2 = columnMatch.matchedLine - columnMatch.margin; + } else { + assert columnMatch.type == SegmentType.RIGHT; + x1 = bounds.w + offsetX + x - 1; + x2 = columnMatch.matchedLine + columnMatch.margin; + } + gc.drawLine(x1, b.y, x1, b.y2()); + gc.drawLine(x2, b.y, x2, b.y2()); + gc.drawString(Integer.toString(columnMatch.margin), + x1 + (x2 - x1 - 16) / 2, centerY - 3); + } else { + gc.useStyle(columnMatch.margin == 0 ? DrawingStyle.DISTANCE + : columnMatch.createCell ? DrawingStyle.GUIDELINE_DASHED + : DrawingStyle.GUIDELINE); + gc.drawLine(x, b.y, x, b.y2()); + } + + // Draw preview rectangle of the first dragged element + 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); + } + } + + /** + * Paints the drag feedback for a grid-mode drag + */ + private void paintGridModeDropFeedback(IGraphics gc, Rect b, GridDropHandler data) { + int radius = mRule.getNewCellSize(); + GridModel grid = data.getGrid(); + + gc.useStyle(DrawingStyle.GUIDELINE); + for (int row = 1; row < grid.actualRowCount; row++) { + int y = grid.getRowY(row); + gc.drawLine(b.x, y - radius, b.x2(), y - radius); + gc.drawLine(b.x, y + radius, b.x2(), y + radius); + + } + for (int column = 1; column < grid.actualColumnCount; column++) { + int x = grid.getColumnX(column); + gc.drawLine(x - radius, b.y, x - radius, b.y2()); + gc.drawLine(x + radius, b.y, x + radius, b.y2()); + } + gc.drawRect(b.x, b.y, b.x2(), b.y2()); + gc.drawRect(b.x + 2 * radius, b.y + 2 * radius, + b.x2() - 2 * radius, b.y2() - 2 * radius); + + int column = data.getColumnMatch().cellIndex; + int row = data.getRowMatch().cellIndex; + boolean createColumn = data.getColumnMatch().createCell; + boolean createRow = data.getRowMatch().createCell; + + Rect cellBounds = grid.getCellBounds(row, column, 1, 1); + + IDragElement first = mElements[0]; + Rect dragBounds = first.getBounds(); + int offsetX = cellBounds.x - dragBounds.x; + int offsetY = cellBounds.y - dragBounds.y; + + gc.useStyle(DrawingStyle.DROP_ZONE_ACTIVE); + if (createColumn) { + gc.fillRect(new Rect(cellBounds.x - radius, + cellBounds.y + (createRow ? -radius : radius), + 2 * radius + 1, cellBounds.h - (createRow ? 0 : 2 * radius))); + offsetX -= radius + dragBounds.w / 2; + } + if (createRow) { + gc.fillRect(new Rect(cellBounds.x + radius, cellBounds.y - radius, + cellBounds.w - 2 * radius, 2 * radius + 1)); + offsetY -= radius + dragBounds.h / 2; + } else if (!createColumn) { + // Choose this cell + gc.fillRect(new Rect(cellBounds.x + radius, cellBounds.y + radius, + cellBounds.w - 2 * radius, cellBounds.h - 2 * radius)); + } + + gc.useStyle(DrawingStyle.DROP_PREVIEW); + mRule.drawElement(gc, first, offsetX, offsetY); + } + } +} 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 new file mode 100644 index 0000000..bf1981e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridMatch.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.common.layout.grid; + +import static com.android.ide.common.layout.grid.GridModel.UNDEFINED; + +import com.android.ide.common.api.SegmentType; + +/** + * A match for a drag within a GridLayout, corresponding to an alignment with another + * edge, or a margin, or centering, or a gap distance from another edge and so on. + */ +class GridMatch implements Comparable<GridMatch> { + /** The distance to the matched edge - used to pick best matches */ + public final int distance; + + /** Type of edge that was matched (this refers to the edge on the dragged node, + * not on the matched node/row/cell etc) */ + public final SegmentType type; + + /** Row or column for the match */ + public int cellIndex; + + /** If true, create a new row/column */ + public boolean createCell; + + /** The actual x or y position of the matched segment */ + public int matchedLine; + + /** Amount of margin between the matched edges */ + public int margin; + + /** + * Constructs a match. + * + * @param type the edge of the dragged element that was matched + * @param distance the absolute distance from the ideal match - used to find the best + * match + * @param matchedLine the actual X or Y location of the ideal match + * @param cellIndex the index of the row or column we matched with + * @param createCell if true, create a new cell by splitting the existing cell at the + * matchedLine position + * @param margin a margin distance to add to the actual location from the matched line + */ + GridMatch(SegmentType type, int distance, int matchedLine, int cellIndex, + boolean createCell, int margin) { + super(); + this.type = type; + this.distance = distance; + this.matchedLine = matchedLine; + this.cellIndex = cellIndex; + this.createCell = createCell; + this.margin = margin; + } + + // Implements Comparable<GridMatch> + public int compareTo(GridMatch o) { + // Pick closest matches first + if (distance != o.distance) { + return distance - o.distance; + } + + // Prefer some types of matches over other matches + return getPriority() - o.getPriority(); + } + + /** + * Describes the match for the user + * + * @return a short description for the user of the match + */ + public String getDisplayName() { + switch (type) { + case BASELINE: + return String.format("Align baseline in row %1$d", cellIndex); + case CENTER_HORIZONTAL: + return "Center horizontally"; + case LEFT: + if (!createCell) { + return String.format("Insert into column %1$d", cellIndex); + } + if (margin != UNDEFINED) { + if (cellIndex == 0) { + return "Add one margin distance from the left"; + } + return String.format("Add next to column %1$d", cellIndex); + } + return String.format("Align left at x=%1$d", matchedLine); + case RIGHT: + if (!createCell) { + return String.format("Insert right-aligned into column %1$d", cellIndex); + } + return String.format("Align right at x=%1$d", matchedLine); + case TOP: + if (!createCell) { + return String.format("Insert into row %1$d", cellIndex); + } + if (margin != UNDEFINED) { + if (cellIndex == 0) { + return "Add one margin distance from the top"; + } + return String.format("Add below row %1$d", cellIndex); + } + return String.format("Align top at y=%1d", matchedLine); + case BOTTOM: + if (!createCell) { + return String.format("Insert into bottom of row %1$d", cellIndex); + } + return String.format("Align bottom at y=%1d", matchedLine); + case CENTER_VERTICAL: + case UNKNOWN: + default: + return null; + } + } + + /** + * Computes the sorting priority of this match, giving baseline matches higher + * precedence than centering which in turn is ordered before external edge matches + */ + private int getPriority() { + switch (type) { + case BASELINE: + return 0; + case CENTER_HORIZONTAL: + case CENTER_VERTICAL: + return 1; + case BOTTOM: + case LEFT: + case RIGHT: + case TOP: + return 2; + } + + return 3; + } +}
\ No newline at end of file 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 new file mode 100644 index 0000000..3cb8ee0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridModel.java @@ -0,0 +1,1959 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.common.layout.grid; + +import static com.android.ide.common.layout.LayoutConstants.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; +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_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.ATTR_ROW_COUNT; +import static com.android.ide.common.layout.LayoutConstants.FQCN_SPACE; +import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; +import static com.android.ide.common.layout.LayoutConstants.VALUE_BOTTOM; +import static com.android.ide.common.layout.LayoutConstants.VALUE_CENTER_VERTICAL; +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 java.lang.Math.abs; +import static java.lang.Math.max; +import static java.lang.Math.min; + +import com.android.ide.common.api.IClientRulesEngine; +import com.android.ide.common.api.INode; +import com.android.ide.common.api.IViewMetadata; +import com.android.ide.common.api.Margins; +import com.android.ide.common.api.Rect; +import com.android.ide.common.layout.GridLayoutRule; +import com.android.util.Pair; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +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 { + /** Marker value used to indicate values (rows, columns, etc) which have not been set */ + static final int UNDEFINED = Integer.MIN_VALUE; + + /** 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; + + /** + * Array of positions (indexed by column) of the left edge of table cells; this + * corresponds to the column positions in the grid + */ + private int[] mLeft; + + /** + * Array of positions (indexed by row) of the top edge of table cells; this + * corresponds to the row positions in the grid + */ + private int[] mTop; + + /** + * Array of positions (indexed by column) of the maximum right hand side bounds of a + * node in the given column; this represents the visual edge of a column even when the + * actual column is wider + */ + private int[] mMaxRight; + + /** + * Array of positions (indexed by row) of the maximum bottom bounds of a node in the + * given row; this represents the visual edge of a row even when the actual row is + * taller + */ + private int[] mMaxBottom; + + /** + * Array of baselines computed for the rows. This array is populated lazily and should + * not be accessed directly; call {@link #getBaseline(int)} instead. + */ + private int[] mBaselines; + + /** List of all the view data for the children in this layout */ + private List<ViewData> mChildViews; + + /** 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; + + /** + * Constructs a {@link GridModel} for the given layout + * + * @param rulesEngine the associated rules engine + * @param node the GridLayout node + */ + public GridModel(IClientRulesEngine rulesEngine, INode node) { + mRulesEngine = rulesEngine; + layout = node; + loadFromXml(); + } + + /** + * Returns the {@link ViewData} for the child at the given index + * + * @param index the position of the child node whose view we want to look up + * @return the corresponding {@link ViewData} + */ + public ViewData getView(int index) { + return mChildViews.get(index); + } + + /** + * Returns the {@link ViewData} for the given child node. + * + * @param node the node for which we want the view info + * @return the view info for the node, or null if not found + */ + public ViewData getView(INode node) { + for (ViewData view : mChildViews) { + if (view.node == node) { + return view; + } + } + + return null; + } + + /** + * Computes the index (among the children nodes) to insert a new node into which + * should be positioned at the given row and column. This will skip over any nodes + * that have implicit positions earlier than the given node, and will also ensure that + * all nodes are placed before the spacer nodes. + * + * @param row the target row of the new node + * @param column the target column of the new node + * @return the insert position to use or -1 if no preference is found + */ + public int getInsertIndex(int row, int column) { + if (vertical) { + for (ViewData view : mChildViews) { + if (view.column > column || view.column == column && view.row >= row) { + return view.index; + } + } + } else { + for (ViewData view : mChildViews) { + if (view.row > row || view.row == row && view.column >= column) { + return view.index; + } + } + } + + // Place it before the first spacer + for (ViewData view : mChildViews) { + if (view.isSpacer()) { + return view.index; + } + } + + return -1; + } + + /** + * Returns the baseline of the given row, or -1 if none is found. This looks for views + * in the row which have baseline vertical alignment and also define their own + * baseline, and returns the first such match. + * + * @param row the row to look up a baseline for + * @return the baseline relative to the row position, or -1 if not defined + */ + public int getBaseline(int row) { + if (row < 0 || row >= mBaselines.length) { + return -1; + } + + int baseline = mBaselines[row]; + if (baseline == UNDEFINED) { + baseline = -1; + + // TBD: Consider stringing together row information in the view data + // so I can quickly identify the views in a given row instead of searching + // among all? + for (ViewData view : mChildViews) { + // We only count baselines for views with rowSpan=1 because + // baseline alignment doesn't work for cell spanning views + if (view.row == row && view.rowSpan == 1) { + baseline = view.node.getBaseline(); + if (baseline != -1) { + // Even views that do have baselines do not count towards a row + // baseline if they have a vertical gravity + String gravity = view.node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_GRAVITY); + if (gravity == null + || !(gravity.contains(VALUE_TOP) + || gravity.contains(VALUE_BOTTOM) + || gravity.contains(VALUE_CENTER_VERTICAL))) { + // Compute baseline relative to the row, not the view itself + baseline += view.node.getBounds().y - getRowY(row); + break; + } + } + } + } + mBaselines[row] = baseline; + } + + return baseline; + } + + /** Applies the row and column values into the XML */ + void applyPositionAttributes() { + for (ViewData view : mChildViews) { + view.applyPositionAttributes(); + } + + // Also fix the columnCount + if (layout.getStringAttr(ANDROID_URI, ATTR_COLUMN_COUNT) != null && + declaredColumnCount > actualColumnCount) { + layout.setAttribute(ANDROID_URI, ATTR_COLUMN_COUNT, + Integer.toString(actualColumnCount)); + } + } + + /** Removes the given flag from a flag attribute value and returns the result */ + static String removeFlag(String flag, String value) { + if (value.equals(flag)) { + return null; + } + // Handle spaces between pipes and flag are a prefix, suffix and interior occurrences + int index = value.indexOf(flag); + if (index != -1) { + int pipe = value.lastIndexOf('|', index); + int endIndex = index + flag.length(); + if (pipe != -1) { + value = value.substring(0, pipe).trim() + value.substring(endIndex).trim(); + } else { + pipe = value.indexOf('|', endIndex); + if (pipe != -1) { + value = value.substring(0, index).trim() + value.substring(pipe + 1).trim(); + } else { + value = value.substring(0, index).trim() + value.substring(endIndex).trim(); + } + } + } + + return value; + } + + /** + * Loads a {@link GridModel} from the XML model. + */ + void loadFromXml() { + INode[] children = layout.getChildren(); + + declaredRowCount = getInt(layout, ATTR_ROW_COUNT, UNDEFINED); + declaredColumnCount = getInt(layout, ATTR_COLUMN_COUNT, UNDEFINED); + // Horizontal is the default, so if no value is specified it is horizontal. + vertical = VALUE_VERTICAL.equals(layout.getStringAttr(ANDROID_URI, ATTR_ORIENTATION)); + + mChildViews = new ArrayList<ViewData>(children.length); + int index = 0; + for (INode child : children) { + ViewData view = new ViewData(child, index++); + mChildViews.add(view); + } + + // Assign row/column positions to all cells that do not explicitly define them + assignRowsAndColumns( + declaredRowCount == UNDEFINED ? children.length : declaredRowCount, + declaredColumnCount == UNDEFINED ? children.length : declaredColumnCount); + + // Compute the actualColumnCount and actualRowCount. This -should- be + // as easy as declaredColumnCount + extraColumnsMap.size(), + // but the user doesn't *have* to declare a column count (or a row count) + // and we need both, so go and find the actual row and column maximums. + int maxColumn = 0; + int maxRow = 0; + for (ViewData view : mChildViews) { + maxColumn = max(maxColumn, view.column); + maxRow = max(maxRow, view.row); + } + actualColumnCount = maxColumn + 1; + actualRowCount = maxRow + 1; + + assignCellBounds(); + for (int i = 0; i <= actualRowCount; i++) { + mBaselines[i] = UNDEFINED; + } + + stale = false; + } + + private Pair<Map<Integer, Integer>, Map<Integer, Integer>> findCellsOutsideDeclaredBounds() { + // See if we have any (row,column) pairs that fall outside the declared + // bounds; for these we identify the number of unique values and assign these + // consecutive values + Map<Integer, Integer> extraColumnsMap = null; + Map<Integer, Integer> extraRowsMap = null; + if (declaredRowCount != UNDEFINED) { + Set<Integer> extraRows = null; + for (ViewData view : mChildViews) { + if (view.row >= declaredRowCount) { + if (extraRows == null) { + extraRows = new HashSet<Integer>(); + } + extraRows.add(view.row); + } + } + if (extraRows != null && declaredRowCount != UNDEFINED) { + List<Integer> rows = new ArrayList<Integer>(extraRows); + Collections.sort(rows); + int row = declaredRowCount; + extraRowsMap = new HashMap<Integer, Integer>(); + for (Integer declared : rows) { + extraRowsMap.put(declared, row++); + } + } + } + if (declaredColumnCount != UNDEFINED) { + Set<Integer> extraColumns = null; + for (ViewData view : mChildViews) { + if (view.column >= declaredColumnCount) { + if (extraColumns == null) { + extraColumns = new HashSet<Integer>(); + } + extraColumns.add(view.column); + } + } + if (extraColumns != null && declaredColumnCount != UNDEFINED) { + List<Integer> columns = new ArrayList<Integer>(extraColumns); + Collections.sort(columns); + int column = declaredColumnCount; + extraColumnsMap = new HashMap<Integer, Integer>(); + for (Integer declared : columns) { + extraColumnsMap.put(declared, column++); + } + } + } + + return Pair.of(extraRowsMap, extraColumnsMap); + } + + /** + * Figure out actual row and column numbers for views that do not specify explicit row + * and/or column numbers + * TODO: Consolidate with the algorithm in GridLayout to ensure we get the + * exact same results! + */ + private void assignRowsAndColumns(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(); + + if (!vertical) { + // Horizontal GridLayout: this is the default. Row and column numbers + // are assigned by assuming that the children are assigned successive + // column numbers until we get to the column count of the grid, at which + // point we jump to the next row. If any cell specifies either an explicit + // row number of column number, we jump to the next available position. + // Note also that if there are any rowspans on the current row, then the + // next row we jump to is below the largest such rowspan - in other words, + // the algorithm does not fill holes in the middle! + + // TODO: Ensure that we don't run into trouble if a later element specifies + // an earlier number... find out what the layout does in that case! + int row = 0; + int column = 0; + int nextRow = 1; + for (ViewData view : mChildViews) { + int declaredColumn = view.column; + if (declaredColumn != UNDEFINED) { + if (declaredColumn >= columnCount) { + assert extraColumnsMap != null; + declaredColumn = extraColumnsMap.get(declaredColumn); + view.column = declaredColumn; + } + if (declaredColumn < column) { + // Must jump to the next row to accommodate the new row + assert nextRow > row; + //row++; + row = nextRow; + } + column = declaredColumn; + } else { + view.column = column; + } + if (view.row != UNDEFINED) { + // TODO: Should this adjust the column number too? (If so must + // also update view.column since we've already processed the local + // column number) + row = view.row; + } else { + view.row = row; + } + + nextRow = Math.max(nextRow, view.row + view.rowSpan); + + // Advance + column += view.columnSpan; + if (column >= columnCount) { + column = 0; + assert nextRow > row; + //row++; + row = nextRow; + } + } + } else { + // Vertical layout: successive children are assigned to the same column in + // successive rows. + int row = 0; + int column = 0; + int nextColumn = 1; + for (ViewData view : mChildViews) { + int declaredRow = view.row; + if (declaredRow != UNDEFINED) { + if (declaredRow >= rowCount) { + declaredRow = extraRowsMap.get(declaredRow); + view.row = declaredRow; + } + if (declaredRow < row) { + // Must jump to the next column to accommodate the new column + assert nextColumn > row; + column = nextColumn; + } + row = declaredRow; + } else { + view.row = row; + } + if (view.column != UNDEFINED) { + // TODO: Should this adjust the row number too? (If so must + // also update view.row since we've already processed the local + // row number) + column = view.column; + } else { + view.column = column; + } + + nextColumn = Math.max(nextColumn, view.column + view.columnSpan); + + // Advance + row += view.rowSpan; + if (row >= rowCount) { + row = 0; + assert nextColumn > column; + //row++; + column = nextColumn; + } + } + } + } + + /** + * Computes the boundaries of the rows and columns by considering the bounds of the + * children. + */ + private void assignCellBounds() { + Rect layoutBounds = layout.getBounds(); + mLeft = new int[actualColumnCount + 1]; + mMaxRight = new int[actualColumnCount + 1]; + for (int i = 1; i < actualColumnCount; i++) { + mLeft[i] = UNDEFINED; + } + mLeft[0] = layoutBounds.x; + mLeft[actualColumnCount] = layoutBounds.x2(); + mTop = new int[actualRowCount + 1]; + mMaxBottom = new int[actualRowCount + 1]; + mBaselines = new int[actualRowCount + 1]; + for (int i = 1; i < actualRowCount; i++) { + mTop[i] = UNDEFINED; + } + mTop[0] = layoutBounds.y; + mTop[actualRowCount] = layoutBounds.y2(); + + for (ViewData view : mChildViews) { + Rect bounds = view.node.getBounds(); + if (!bounds.isValid()) { + continue; + } + int column = view.column; + int row = view.row; + + if (mLeft[column] == UNDEFINED) { + mLeft[column] = bounds.x; + } else { + mLeft[column] = Math.min(bounds.x, mLeft[column]); + } + if (mTop[row] == UNDEFINED) { + mTop[row] = bounds.y; + } else { + mTop[row] = Math.min(bounds.y, mTop[row]); + } + + if (!view.isSpacer()) { + int x2 = bounds.x2(); + int y2 = bounds.y2(); + int targetColumn = min(actualColumnCount - 1, column + view.columnSpan - 1); + int targetRow = min(actualRowCount - 1, row + view.rowSpan - 1); + IViewMetadata metadata = mRulesEngine.getMetadata(view.node.getFqcn()); + if (metadata != null) { + Margins insets = metadata.getInsets(); + if (insets != null) { + x2 -= insets.right; + y2 -= insets.bottom; + } + } + if (mMaxRight[targetColumn] < x2) { + mMaxRight[targetColumn] = x2; + } + if (mMaxBottom[targetRow] < y2) { + mMaxBottom[targetRow] = y2; + } + } + } + + // Ensure that any empty columns/rows have a valid boundary value; for now, + for (int i = actualColumnCount - 1; i >= 0; i--) { + if (mLeft[i] == UNDEFINED) { + if (i == 0) { + mLeft[i] = layoutBounds.x; + } else if (i < actualColumnCount - 1) { + mLeft[i] = mLeft[i + 1] - 1; + if (mLeft[i - 1] != UNDEFINED && mLeft[i] < mLeft[i - 1]) { + mLeft[i] = mLeft[i - 1]; + } + } else { + mLeft[i] = layoutBounds.x2(); + } + } + } + for (int i = actualRowCount - 1; i >= 0; i--) { + if (mTop[i] == UNDEFINED) { + if (i == 0) { + mTop[i] = layoutBounds.y; + } else if (i < actualRowCount - 1) { + mTop[i] = mTop[i + 1] - 1; + if (mTop[i - 1] != UNDEFINED && mTop[i] < mTop[i - 1]) { + mTop[i] = mTop[i - 1]; + } + } else { + mTop[i] = layoutBounds.y2(); + } + } + } + + // The bounds should be in ascending order now + for (int i = 1; i < actualRowCount; i++) { + assert mTop[i + 1] >= mTop[i]; + } + for (int i = 0; i < actualColumnCount; i++) { + assert mLeft[i + 1] >= mLeft[i]; + } + } + + /** + * Add a new column. + * + * @param selectedChildren if null or empty, add the column at the end of the grid, + * and otherwise add it before the column of the first selected child + * @return the newly added column spacer + */ + public INode addColumn(List<? extends INode> selectedChildren) { + // Determine insert index + int newColumn = actualColumnCount; + if (selectedChildren != null && selectedChildren.size() > 0) { + INode first = selectedChildren.get(0); + ViewData view = getView(first); + newColumn = view.column; + } + + INode newView = addColumn(newColumn, null, UNDEFINED, false, UNDEFINED, UNDEFINED); + if (newView != null) { + mRulesEngine.select(Collections.singletonList(newView)); + } + + return newView; + } + + /** + * Adds a new column. + * + * @param newColumn the column index to insert before + * @param newView the {@link INode} to insert as the column spacer, which may be null + * (in which case a spacer is automatically created) + * @param columnWidthDp the width, in device independent pixels, of the column to be + * added (which may be {@link #UNDEFINED} + * @param split if true, split the existing column into two at the given x position + * @param row the row to add the newView to + * @param x the x position of the column we're inserting + * @return the column spacer + */ + public INode addColumn(int newColumn, INode newView, int columnWidthDp, + boolean split, int row, int x) { + assert !stale; + stale = true; + + // Insert a new column + if (declaredColumnCount != UNDEFINED) { + declaredColumnCount++; + layout.setAttribute(ANDROID_URI, ATTR_COLUMN_COUNT, + Integer.toString(declaredColumnCount)); + } + + boolean isLastColumn = true; + for (ViewData view : mChildViews) { + if (view.column >= newColumn) { + isLastColumn = false; + break; + } + } + + for (ViewData view : mChildViews) { + boolean columnSpanSet = false; + + int endColumn = view.column + view.columnSpan; + if (view.column >= newColumn || endColumn == newColumn) { + if (view.column == newColumn || endColumn == newColumn) { + //if (view.row == 0) { + if (newView == null && !isLastColumn) { + // Insert a new spacer + int index = getChildIndex(layout.getChildren(), view.node); + assert view.index == index; // TODO: Get rid of getter + if (endColumn == newColumn) { + // This cell -ends- at the desired position: insert it after + index++; + } + + newView = addSpacer(layout, index, + split ? row : UNDEFINED, + split ? newColumn - 1 : UNDEFINED, + columnWidthDp != UNDEFINED ? columnWidthDp : DEFAULT_CELL_WIDTH, + DEFAULT_CELL_HEIGHT); + } + + // 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. + if (view.column == newColumn) { + view.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN, + Integer.toString(view.column + 1)); + } // else: endColumn == newColumn: handled below + } else if (view.node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN) != null) { + view.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN, + Integer.toString(view.column + 1)); + } + } else if (endColumn > newColumn) { + setColumnSpanAttribute(view.node, view.columnSpan + 1); + columnSpanSet = true; + } + + if (split && !columnSpanSet && view.node.getBounds().x2() > x) { + if (view.node.getBounds().x < x) { + setColumnSpanAttribute(view.node, view.columnSpan + 1); + } + } + } + + // Hardcode the row numbers if the last column is a new column such that + // they don't jump back to backfill the previous row's new last cell + if (isLastColumn) { + for (ViewData view : mChildViews) { + if (view.column == 0 && view.row > 0) { + view.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, + Integer.toString(view.row)); + } + } + if (split) { + assert newView == null; + addSpacer(layout, -1, row, newColumn -1, + columnWidthDp != UNDEFINED ? columnWidthDp : DEFAULT_CELL_WIDTH, + SPACER_SIZE_DP); + } + } + + return newView; + } + + /** + * Removes the columns containing the given selection + * + * @param selectedChildren a list of nodes whose columns should be deleted + */ + public void removeColumns(List<? extends INode> selectedChildren) { + if (selectedChildren.size() == 0) { + return; + } + + assert !stale; + stale = true; + + // Figure out which columns should be removed + Set<Integer> removedSet = new HashSet<Integer>(); + for (INode child : selectedChildren) { + ViewData view = getView(child); + removedSet.add(view.column); + } + // Sort them in descending order such that we can process each + // deletion independently + List<Integer> removed = new ArrayList<Integer>(removedSet); + Collections.sort(removed, Collections.reverseOrder()); + + for (int removedColumn : removed) { + // Remove column. + // First, adjust column count. + // 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? + if (declaredColumnCount != UNDEFINED) { + declaredColumnCount--; + layout.setAttribute(ANDROID_URI, ATTR_COLUMN_COUNT, + Integer.toString(declaredColumnCount)); + } + + // Remove any elements that begin in the deleted columns... + // If they have colspan > 1, then we must insert a spacer instead. + // For any other elements that overlap, we need to subtract from the span. + + for (ViewData view : mChildViews) { + if (view.column == removedColumn) { + int index = getChildIndex(layout.getChildren(), view.node); + assert view.index == index; // TODO: Get rid of getter + if (view.columnSpan > 1) { + // Make a new spacer which is the width of the following + // columns + int columnWidth = getColumnWidth(removedColumn, view.columnSpan) - + getColumnWidth(removedColumn, 1); + int columnWidthDip = mRulesEngine.pxToDp(columnWidth); + addSpacer(layout, index, UNDEFINED, UNDEFINED, columnWidthDip, + SPACER_SIZE_DP); + } + 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); + } else if (view.column > removedColumn) { + if (view.node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN) != null) { + view.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN, + Integer.toString(view.column - 1)); + } + } + } + } + } + + /** + * Add a new row. + * + * @param selectedChildren if null or empty, add the row at the bottom of the grid, + * and otherwise add it before the row of the first selected child + * @return the newly added row spacer + */ + public INode addRow(List<? extends INode> selectedChildren) { + // Determine insert index + int newRow = actualRowCount; + if (selectedChildren.size() > 0) { + INode first = selectedChildren.get(0); + ViewData view = getView(first); + newRow = view.row; + } + + INode newView = addRow(newRow, null, UNDEFINED, false, UNDEFINED, UNDEFINED); + if (newView != null) { + mRulesEngine.select(Collections.singletonList(newView)); + } + + return newView; + } + + /** + * Adds a new column. + * + * @param newRow the row index to insert before + * @param newView the {@link INode} to insert as the row spacer, which may be null (in + * which case a spacer is automatically created) + * @param rowHeightDp the height, in device independent pixels, of the row to be added + * (which may be {@link #UNDEFINED} + * @param split if true, split the existing row into two at the given y position + * @param column the column to add the newView to + * @param y the y position of the row we're inserting + * @return the row spacer + */ + 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; + + if (declaredRowCount != UNDEFINED) { + declaredRowCount++; + layout.setAttribute(ANDROID_URI, ATTR_ROW_COUNT, + Integer.toString(declaredRowCount)); + } + boolean added = false; + for (ViewData view : mChildViews) { + if (view.row >= newRow) { + // Adjust the column count + if (view.row == newRow && view.column == 0) { + // Insert a new spacer + if (newView == null) { + int index = getChildIndex(layout.getChildren(), view.node); + assert view.index == index; // TODO: Get rid of getter + if (declaredColumnCount != UNDEFINED && !split) { + layout.setAttribute(ANDROID_URI, ATTR_COLUMN_COUNT, + Integer.toString(declaredColumnCount)); + } + newView = addSpacer(layout, index, + split ? newRow - 1 : UNDEFINED, + split ? column : UNDEFINED, + SPACER_SIZE_DP, + rowHeightDp != UNDEFINED ? rowHeightDp : DEFAULT_CELL_HEIGHT); + } + + // 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. + view.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, + Integer.toString(view.row + 1)); + + added = true; + } else if (view.node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_ROW) != null) { + view.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, + Integer.toString(view.row + 1)); + } + } else { + int endRow = view.row + view.rowSpan; + if (endRow > newRow) { + setRowSpanAttribute(view.node, view.rowSpan + 1); + } else if (split && view.node.getBounds().y2() > y) { + if (view.node.getBounds().y < y) { + setRowSpanAttribute(view.node, view.rowSpan + 1); + } + } + } + } + + if (!added) { + // Append a row at the end + if (newView == null) { + newView = addSpacer(layout, -1, UNDEFINED, UNDEFINED, + SPACER_SIZE_DP, + rowHeightDp != UNDEFINED ? rowHeightDp : DEFAULT_CELL_HEIGHT); + } + if (declaredColumnCount != UNDEFINED && !split) { + newView.setAttribute(ANDROID_URI, ATTR_COLUMN_COUNT, + Integer.toString(declaredColumnCount)); + } + if (split) { + newView.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, Integer.toString(newRow - 1)); + newView.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN, Integer.toString(column)); + } + } + + return newView; + } + + /** + * Removes the rows containing the given selection + * + * @param selectedChildren a list of nodes whose rows should be deleted + */ + public void removeRows(List<? extends INode> selectedChildren) { + if (selectedChildren.size() == 0) { + return; + } + + assert !stale; + stale = true; + + // Figure out which rows should be removed + Set<Integer> removedSet = new HashSet<Integer>(); + for (INode child : selectedChildren) { + ViewData view = getView(child); + removedSet.add(view.row); + } + // Sort them in descending order such that we can process each + // deletion independently + List<Integer> removed = new ArrayList<Integer>(removedSet); + Collections.sort(removed, Collections.reverseOrder()); + + for (int removedRow : removed) { + // Remove row. + // First, adjust row count. + // TODO: Don't do this if the row being deleted is outside + // the declared row range! + if (declaredRowCount != UNDEFINED) { + declaredRowCount--; + layout.setAttribute(ANDROID_URI, ATTR_ROW_COUNT, + Integer.toString(declaredRowCount)); + } + + // Remove any elements that begin in the deleted rows... + // If they have colspan > 1, then we must hardcode a new row number + // instead. + // For any other elements that overlap, we need to subtract from the span. + + for (ViewData view : mChildViews) { + if (view.row == removedRow) { + // 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 + layout.removeChild(view.node); + } else if (view.row > removedRow) { + if (view.node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_ROW) != null) { + view.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, + Integer.toString(view.row - 1)); + } + } else if (view.row < removedRow + && view.row + view.rowSpan > removedRow) { + // Subtract row span to skip this item + setRowSpanAttribute(view.node, view.rowSpan - 1); + } + } + } + } + + /** + * Returns the row containing the given y line + * + * @param y the vertical position + * @return the row containing the given line + */ + public int getRow(int y) { + int row = Arrays.binarySearch(mTop, y); + if (row == -1) { + // Smaller than the first element; just use the first row + return 0; + } else if (row < 0) { + row = -(row + 2); + } + + return row; + } + + /** + * Returns the column containing the given x line + * + * @param x the horizontal position + * @return the column containing the given line + */ + public int getColumn(int x) { + int column = Arrays.binarySearch(mLeft, x); + if (column == -1) { + // Smaller than the first element; just use the first column + return 0; + } else if (column < 0) { + column = -(column + 2); + } + + return column; + } + + /** + * Returns the closest row to the given y line. This is + * either the row containing the line, or the row below it. + * + * @param y the vertical position + * @return the closest row + */ + public int getClosestRow(int y) { + int row = Arrays.binarySearch(mTop, y); + if (row == -1) { + // Smaller than the first element; just use the first column + return 0; + } else if (row < 0) { + row = -(row + 2); + } + + if (getRowDistance(row, y) < getRowDistance(row + 1, y)) { + return row; + } else { + return row + 1; + } + } + + /** + * Returns the closest column to the given x line. This is + * either the column containing the line, or the column following it. + * + * @param x the horizontal position + * @return the closest column + */ + public int getClosestColumn(int x) { + int column = Arrays.binarySearch(mLeft, x); + if (column == -1) { + // Smaller than the first element; just use the first column + return 0; + } else if (column < 0) { + column = -(column + 2); + } + + if (getColumnDistance(column, x) < getColumnDistance(column + 1, x)) { + return column; + } else { + return column + 1; + } + } + + /** + * Returns the distance between the given x position and the beginning of the given column + * + * @param column the column + * @param x the x position + * @return the distance between the two + */ + public int getColumnDistance(int column, int x) { + return abs(getColumnX(column) - x); + } + + /** + * Returns the actual width of the given column. This returns the difference between + * the rightmost edge of the views (not including spacers) and the left edge of the + * column. + * + * @param column the column + * @return the actual width of the non-spacer views in the column + */ + public int getColumnActualWidth(int column) { + return getColumnMaxX(column) - getColumnX(column); + } + + /** + * Returns the distance between the given y position and the top of the given row + * + * @param row the row + * @param y the y position + * @return the distance between the two + */ + public int getRowDistance(int row, int y) { + return abs(getRowY(row) - y); + } + + /** + * Returns the y position of the top of the given row + * + * @param row the target row + * @return the y position of its top edge + */ + public int getRowY(int row) { + return mTop[min(mTop.length - 1, max(0, row))]; + } + + /** + * Returns the bottom-most edge of any of the non-spacer children in the given row + * + * @param row the target row + * @return the bottom-most edge of any of the non-spacer children in the row + */ + public int getRowMaxY(int row) { + return mMaxBottom[min(mMaxBottom.length - 1, max(0, row))]; + } + + /** + * Returns the actual height of the given row. This returns the difference between + * the bottom-most edge of the views (not including spacers) and the top edge of the + * row. + * + * @param row the row + * @return the actual height of the non-spacer views in the row + */ + public int getRowActualHeight(int row) { + return getRowMaxY(row) - getRowY(row); + } + + /** + * Returns a list of all the nodes that intersects the rows in the range + * {@code y1 <= y <= y2}. + * + * @param y1 the starting y, inclusive + * @param y2 the ending y, inclusive + * @return a list of nodes intersecting the given rows, never null but possibly empty + */ + public Collection<INode> getIntersectsRow(int y1, int y2) { + List<INode> nodes = new ArrayList<INode>(); + + for (ViewData view : mChildViews) { + if (!view.isSpacer()) { + Rect bounds = view.node.getBounds(); + if (bounds.y2() >= y1 && bounds.y <= y2) { + nodes.add(view.node); + } + } + } + + return nodes; + } + + /** + * Returns the height of the given row or rows (if the rowSpan is greater than 1) + * + * @param row the target row + * @param rowSpan the row span + * @return the height in pixels of the given rows + */ + public int getRowHeight(int row, int rowSpan) { + return getRowY(row + rowSpan) - getRowY(row); + } + + /** + * Returns the x position of the left edge of the given column + * + * @param column the target column + * @return the x position of its left edge + */ + public int getColumnX(int column) { + return mLeft[min(mLeft.length - 1, max(0, column))]; + } + + /** + * Returns the rightmost edge of any of the non-spacer children in the given row + * + * @param column the target column + * @return the rightmost edge of any of the non-spacer children in the column + */ + public int getColumnMaxX(int column) { + return mMaxRight[min(mMaxRight.length - 1, max(0, column))]; + } + + /** + * Returns the width of the given column or columns (if the columnSpan is greater than 1) + * + * @param column the target column + * @param columnSpan the column span + * @return the width in pixels of the given columns + */ + public int getColumnWidth(int column, int columnSpan) { + return getColumnX(column + columnSpan) - getColumnX(column); + } + + /** + * Returns the bounds of the cell at the given row and column position, with the given + * row and column spans. + * + * @param row the target row + * @param column the target column + * @param rowSpan the row span + * @param columnSpan the column span + * @return the bounds, in pixels, of the given cell + */ + public Rect getCellBounds(int row, int column, int rowSpan, int columnSpan) { + return new Rect(getColumnX(column), getRowY(row), + getColumnWidth(column, columnSpan), + getRowHeight(row, rowSpan)); + } + + /** + * Produces a display of view contents along with the pixel positions of each + * row/column, like the following (used for diagnostics only) + * + * <pre> + * |0 |49 |143 |192 |240 + * 36| | |button2 | + * 72| |radioButton1 |button2 | + * 74|button1 |radioButton1 |button2 | + * 108|button1 | |button2 | + * 110| | |button2 | + * 149| | | | + * 320 + * </pre> + */ + @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; + + List<List<List<ViewData>>> rowList = new ArrayList<List<List<ViewData>>>(mTop.length); + for (int row = 0; row < mTop.length; row++) { + List<List<ViewData>> columnList = new ArrayList<List<ViewData>>(mLeft.length); + for (int col = 0; col < mLeft.length; col++) { + columnList.add(new ArrayList<ViewData>(4)); + } + 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; + } + if (rowList.size() <= view.row + i) { + break; + } + for (int j = 0; j < view.columnSpan; j++) { + List<List<ViewData>> columnList = rowList.get(view.row + i); + if (columnList.size() <= view.column + j) { + break; + } + columnList.get(view.column + j).add(view); + } + } + } + + StringWriter stringWriter = new StringWriter(); + PrintWriter out = new PrintWriter(stringWriter); + out.printf("%" + cellWidth + "s", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + for (int col = 0; col < actualColumnCount + 1; col++) { + out.printf("|%-" + (cellWidth - 1) + "d", mLeft[col]); //$NON-NLS-1$ //$NON-NLS-2$ + } + out.printf("\n"); //$NON-NLS-1$ + for (int row = 0; row < actualRowCount + 1; row++) { + out.printf("%" + cellWidth + "d", mTop[row]); //$NON-NLS-1$ //$NON-NLS-2$ + if (row == actualRowCount) { + break; + } + for (int col = 0; col < actualColumnCount; col++) { + List<ViewData> views = rowList.get(row).get(col); + + StringBuilder sb = new StringBuilder(); + for (ViewData view : views) { + String id = view != null ? view.getId() : ""; //$NON-NLS-1$ + if (id.startsWith(NEW_ID_PREFIX)) { + id = id.substring(NEW_ID_PREFIX.length()); + } + if (id.length() > cellWidth - 2) { + id = id.substring(0, cellWidth - 2); + } + if (sb.length() > 0) { + sb.append(","); //$NON-NLS-1$ + } + sb.append(id); + } + String cellString = sb.toString(); + if (cellString.contains(",") && cellString.length() > cellWidth - 2) { //$NON-NLS-1$ + cellString = cellString.substring(0, cellWidth - 6) + "...,"; //$NON-NLS-1$ + } + out.printf("|%-" + (cellWidth - 2) + "s ", cellString); //$NON-NLS-1$ //$NON-NLS-2$ + } + out.printf("\n"); //$NON-NLS-1$ + } + + out.flush(); + return stringWriter.toString(); + } + + /** + * Split a cell into two or three columns. + * + * @param newColumn The column number to insert before + * @param insertMarginColumn If false, then the cell at newColumn -1 is split with the + * left part taking up exactly columnWidthDp dips. If true, then the column + * is split twice; the left part is the implicit width of the column, the + * new middle (margin) column is exactly the columnWidthDp size and the + * right column is the remaining space of the old cell. + * @param columnWidthDp The width of the column inserted before the new column (or if + * insertMarginColumn is false, then the width of the margin column) + * @param x the x coordinate of the new column + */ + public void splitColumn(int newColumn, boolean insertMarginColumn, int columnWidthDp, int x) { + assert !stale; + stale = true; + + // Insert a new column + if (declaredColumnCount != UNDEFINED) { + declaredColumnCount++; + if (insertMarginColumn) { + declaredColumnCount++; + } + layout.setAttribute(ANDROID_URI, ATTR_COLUMN_COUNT, + Integer.toString(declaredColumnCount)); + } + + // Are we inserting a new last column in the grid? That requires some special handling... + boolean isLastColumn = true; + for (ViewData view : mChildViews) { + if (view.column >= newColumn) { + isLastColumn = false; + break; + } + } + + // Hardcode the row numbers if the last column is a new column such that + // they don't jump back to backfill the previous row's new last cell: + // TODO: Only do this for horizontal layouts! + if (isLastColumn) { + for (ViewData view : mChildViews) { + if (view.column == 0 && view.row > 0) { + if (view.node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_ROW) == null) { + view.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, + Integer.toString(view.row)); + } + } + } + } + + // Find the spacer which marks this column, and if found, mark it as a split + ViewData prevColumnSpacer = null; + for (ViewData view : mChildViews) { + if (view.column == newColumn - 1 && view.isColumnSpacer()) { + prevColumnSpacer = view; + break; + } + } + + // Process all existing grid elements: + // * Increase column numbers for all columns that have a hardcoded column number + // greater than the new column + // * Set an explicit column=0 where needed (TODO: Implement this) + // * Increase the columnSpan for all columns that overlap the newly inserted column edge + // * Split the spacer which defined the size of this column into two + // (and if not found, create a new spacer) + // + for (ViewData view : mChildViews) { + if (view == prevColumnSpacer) { + continue; + } + + INode node = view.node; + int column = view.column; + if (column > newColumn || (column == newColumn && view.node.getBounds().x2() > x)) { + // ALWAYS set the column, because + // (1) if it has been set, it needs to be corrected + // (2) if it has not been set, it needs to be set to cause this column + // to skip over the new column (there may be no views for the new + // column on this row). + // TODO: Enhance this such that we only set the column to a skip number + // where necessary, e.g. only on the FIRST view on this row following the + // skipped column! + + //if (node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN) != null) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN, + Integer.toString(column + (insertMarginColumn ? 2 : 1))); + //} + } else if (!view.isSpacer()) { + int endColumn = column + view.columnSpan; + if (endColumn > newColumn + || endColumn == newColumn && view.node.getBounds().x2() > x) { + // This cell spans the new insert position, so increment the column span + setColumnSpanAttribute(node, view.columnSpan + (insertMarginColumn ? 2 : 1)); + } + } + } + + // Insert new spacer: + if (prevColumnSpacer != null) { + int px = getColumnWidth(newColumn - 1, 1); + if (insertMarginColumn || columnWidthDp == 0) { + px -= getColumnActualWidth(newColumn - 1); + } + int dp = mRulesEngine.pxToDp(px); + int remaining = dp - columnWidthDp; + if (remaining > 0) { + prevColumnSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, + String.format(VALUE_N_DP, remaining)); + prevColumnSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN, + Integer.toString(insertMarginColumn ? newColumn + 1 : newColumn)); + } + } + + if (columnWidthDp > 0) { + int index = prevColumnSpacer != null ? prevColumnSpacer.index : -1; + + addSpacer(layout, index, 0, insertMarginColumn ? newColumn : newColumn - 1, + columnWidthDp, SPACER_SIZE_DP); + } + } + + /** + * Split a cell into two or three rows. + * + * @param newRow The row number to insert before + * @param insertMarginRow If false, then the cell at newRow -1 is split with the above + * part taking up exactly rowHeightDp dips. If true, then the row is split + * twice; the top part is the implicit height of the row, the new middle + * (margin) row is exactly the rowHeightDp size and the bottom column is + * the remaining space of the old cell. + * @param rowHeightDp The height of the row inserted before the new row (or if + * insertMarginRow is false, then the height of the margin row) + * @param y the y coordinate of the new row + */ + public void splitRow(int newRow, boolean insertMarginRow, int rowHeightDp, int y) { + // Insert a new row + if (declaredRowCount != UNDEFINED) { + declaredRowCount++; + if (insertMarginRow) { + declaredRowCount++; + } + layout.setAttribute(ANDROID_URI, ATTR_ROW_COUNT, + Integer.toString(declaredRowCount)); + } + + // Find the spacer which marks this row, and if found, mark it as a split + ViewData prevRowSpacer = null; + for (ViewData view : mChildViews) { + if (view.row == newRow - 1 && view.isRowSpacer()) { + prevRowSpacer = view; + break; + } + } + + // Se splitColumn() for details + for (ViewData view : mChildViews) { + if (view == prevRowSpacer) { + continue; + } + + INode node = view.node; + int row = view.row; + if (row > newRow || (row == newRow && view.node.getBounds().y2() > y)) { + //if (node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_ROW) != null) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, + Integer.toString(row + (insertMarginRow ? 2 : 1))); + //} + } else if (!view.isSpacer()) { + int endRow = row + view.rowSpan; + if (endRow > newRow + || endRow == newRow && view.node.getBounds().y2() > y) { + // This cell spans the new insert position, so increment the row span + setRowSpanAttribute(node, view.rowSpan + (insertMarginRow ? 2 : 1)); + } + } + } + + // Insert new spacer: + if (prevRowSpacer != null) { + int px = getRowHeight(newRow - 1, 1); + if (insertMarginRow || rowHeightDp == 0) { + px -= getRowActualHeight(newRow - 1); + } + int dp = mRulesEngine.pxToDp(px); + int remaining = dp - rowHeightDp; + if (remaining > 0) { + prevRowSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, + String.format(VALUE_N_DP, remaining)); + prevRowSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, + Integer.toString(insertMarginRow ? newRow + 1 : newRow)); + } + } + + if (rowHeightDp > 0) { + int index = prevRowSpacer != null ? prevRowSpacer.index : -1; + addSpacer(layout, index, insertMarginRow ? newRow : newRow - 1, + 0, SPACER_SIZE_DP, rowHeightDp); + } + } + + /** + * Data about a view in a table; this is not the same as a cell because multiple views + * can share a single cell, and a view can span many cells. + */ + static class ViewData { + public final INode node; + public final int index; + public int row; + public int column; + public int rowSpan; + public int columnSpan; + //public final float rowWeight; + //public final float columnWeight; + + ViewData(INode n, int index) { + node = n; + this.index = index; + + column = getInt(n, ATTR_LAYOUT_COLUMN, UNDEFINED); + columnSpan = getInt(n, ATTR_LAYOUT_COLUMN_SPAN, 1); + row = getInt(n, ATTR_LAYOUT_ROW, UNDEFINED); + rowSpan = getInt(n, ATTR_LAYOUT_ROW_SPAN, 1); + + // Weights are in flux + // + //String width = n.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WIDTH); + //float colDefaultWeight; + //if (VALUE_MATCH_PARENT.equals(width) || VALUE_FILL_PARENT.equals(width)) { + // colDefaultWeight = 1.0f; + //} else { + // colDefaultWeight = 0.0f; + //} + //String height = n.getStringAttr(ANDROID_URI, ATTR_LAYOUT_HEIGHT); + //float rowDefaultWeight; + //if (VALUE_MATCH_PARENT.equals(height) || VALUE_FILL_PARENT.equals(height)) { + // rowDefaultWeight = 1.0f; + //} else { + // rowDefaultWeight = 0.0f; + //} + // + //columnWeight = getFloat(n, ATTR_LAYOUT_COLUMN_WEIGHT, colDefaultWeight); + //rowWeight = getFloat(n, ATTR_LAYOUT_ROW_WEIGHT, rowDefaultWeight); + + // Interval hSpan = new Interval(column, column + columnSpan); + // this.columnGroup = new Group(hSpan, getColumnAlignment(gravity, width)); + // Interval vSpan = new Interval(row, row + rowSpan); + // this.rowGroup = new Group(vSpan, getRowAlignment(gravity, height)); + } + + /** Applies the column and row fields into the XML model */ + void applyPositionAttributes() { + if (node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_COLUMN) == null) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN, + Integer.toString(column)); + } + if (node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_ROW) == null) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, + Integer.toString(row)); + } + } + + /** Returns the id of this node, or makes one up for display purposes */ + String getId() { + String id = node.getStringAttr(ANDROID_URI, ATTR_ID); + if (id == null) { + id = "<unknownid>"; //$NON-NLS-1$ + String fqn = node.getFqcn(); + fqn = fqn.substring(fqn.lastIndexOf('.') + 1); + id = fqn + "-" + + Integer.toString(System.identityHashCode(node)).substring(0, 3); + } + + return id; + } + + /** Returns true if this {@link ViewData} represents a spacer */ + boolean isSpacer() { + return FQCN_SPACE.equals(node.getFqcn()); + } + + /** + * Returns true if this {@link ViewData} represents a column spacer + */ + boolean isColumnSpacer() { + return isSpacer() && + // Any spacer not found in column 0 is a column spacer since we + // place all horizontal spacers in column 0 + ((column > 0) + // TODO: Find a cleaner way. Maybe set ids on the elements in (0,0) and + // for column distinguish by id. Or at least only do this for column 0! + || !SPACER_SIZE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WIDTH))); + } + + /** + * Returns true if this {@link ViewData} represents a row spacer + */ + boolean isRowSpacer() { + return isSpacer() && + // Any spacer not found in row 0 is a row spacer since we + // place all vertical spacers in row 0 + ((row > 0) + // TODO: Find a cleaner way. Maybe set ids on the elements in (0,0) and + // for column distinguish by id. Or at least only do this for column 0! + || !SPACER_SIZE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_HEIGHT))); + } + } + + /** + * Sets the column span of the given node to the given value (or if the value is 1, + * removes it) + * + * @param node the target node + * @param span the new column span + */ + public static void setColumnSpanAttribute(INode node, int span) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN_SPAN, + span > 1 ? Integer.toString(span) : null); + } + + /** + * Sets the row span of the given node to the given value (or if the value is 1, + * removes it) + * + * @param node the target node + * @param span the new row span + */ + public static void setRowSpanAttribute(INode node, int span) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW_SPAN, + span > 1 ? Integer.toString(span) : null); + } + + /** Returns the index of the given target node in the given child node array */ + static int getChildIndex(INode[] children, INode target) { + int index = 0; + for (INode child : children) { + if (child == target) { + return index; + } + index++; + } + + return -1; + } + + /** + * 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. + * + * @param child the child that is going to be removed shortly + */ + public void markDeleted(INode child) { + if (mDeleted == null) { + mDeleted = new HashSet<INode>(); + } + + mDeleted.add(child); + } + + /** + * 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<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); + + for (ViewData view : mChildViews) { + if (view.isColumnSpacer()) { + columnSpacers.put(view.column, view); + } else if (view.isRowSpacer()) { + rowSpacers.put(view.row, view); + } else if (!mDeleted.contains(view.node)) { + usedColumns.add(Integer.valueOf(view.column)); + usedRows.add(Integer.valueOf(view.row)); + } + } + + if (usedColumns.size() == 0) { + // No more views - just remove all the spacers + for (ViewData spacer : columnSpacers.values()) { + layout.removeChild(spacer.node); + } + for (ViewData spacer : rowSpacers.values()) { + layout.removeChild(spacer.node); + } + 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 + nextSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN, + Integer.toString(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; + } + + if (spacer != null) { + // Combine spacer and prevSpacer. + mergeSpacers(prevSpacer, spacer, false /*row*/); + } + + // 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--; + view.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN, + Integer.toString(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); + } + } + } + } + } + + 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)); + nextSpacer.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, + Integer.toString(row)); + rowSpacers.put(row, nextSpacer); + } else { + continue; + } + } else if (prevSpacer == null) { + continue; + } + + if (spacer != null) { + // Combine spacer and prevSpacer. + mergeSpacers(prevSpacer, spacer, true /*row*/); + } + + + // 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--; + view.node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, + Integer.toString(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); + } + } + } + } + } + + // TODO: Reduce row/column counts! + } + + /** + * 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); + } + + /** + * 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. + } + } + } + + // 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); + } + + /** + * Adds a spacer to the given parent, at the given index. + * + * @param parent the GridLayout + * @param index the index to insert the spacer at, or -1 to append + * @param row the row to add the spacer to (or {@link #UNDEFINED} to not set a row yet + * @param column the column to add the spacer to (or {@link #UNDEFINED} to not set a + * column yet + * @param widthDp the width in device independent pixels to assign to the spacer + * @param heightDp the height in device independent pixels to assign to the spacer + * @return the newly added spacer + */ + static INode addSpacer(INode parent, int index, int row, int column, + int widthDp, int heightDp) { + INode spacer; + if (index != -1) { + spacer = parent.insertChildAt(FQCN_SPACE, index); + } else { + spacer = parent.appendChild(FQCN_SPACE); + } + if (row != UNDEFINED) { + spacer.setAttribute(ANDROID_URI, ATTR_LAYOUT_ROW, Integer.toString(row)); + } + if (column != UNDEFINED) { + spacer.setAttribute(ANDROID_URI, ATTR_LAYOUT_COLUMN, Integer.toString(column)); + } + if (widthDp > 0) { + spacer.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, + String.format(VALUE_N_DP, widthDp)); + } + if (heightDp > 0) { + spacer.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, + String.format(VALUE_N_DP, heightDp)); + } + + // Temporary hack + if (GridLayoutRule.sDebugGridLayout) { + //String id = NEW_ID_PREFIX + "s"; + //if (row == 0) { + // id += "c"; + //} + //if (column == 0) { + // id += "r"; + //} + //if (row > 0) { + // id += Integer.toString(row); + //} + //if (column > 0) { + // id += Integer.toString(column); + //} + String id = NEW_ID_PREFIX + "spacer_" //$NON-NLS-1$ + + Integer.toString(System.identityHashCode(spacer)).substring(0, 3); + spacer.setAttribute(ANDROID_URI, ATTR_ID, id); + } + + + return spacer; + } + + /** + * Returns the integer value of the given attribute, or the given defaultValue if the + * attribute was not set. + * + * @param node the target node + * @param attribute the attribute name (which must be in the android: namespace) + * @param defaultValue the default value to use if the value is not set + * @return the attribute integer value + */ + private static int getInt(INode node, String attribute, int defaultValue) { + String valueString = node.getStringAttr(ANDROID_URI, attribute); + if (valueString != null) { + try { + return Integer.decode(valueString); + } catch (NumberFormatException nufe) { + // Ignore - error in user's XML + } + } + + return defaultValue; + } + + /** + * Returns the float value of the given attribute, or the given defaultValue if the + * attribute was not set. + * + * @param node the target node + * @param attribute the attribute name (which must be in the android: namespace) + * @param defaultValue the default value to use if the value is not set + * @return the attribute float value + */ + private static float getFloat(INode node, String attribute, float defaultValue) { + String valueString = node.getStringAttr(ANDROID_URI, attribute); + if (valueString != null) { + try { + return Float.parseFloat(valueString); + } catch (NumberFormatException nufe) { + // Ignore - error in user's XML + } + } + + return defaultValue; + } + + /** + * Returns the boolean value of the given attribute, or the given defaultValue if the + * attribute was not set. + * + * @param node the target node + * @param attribute the attribute name (which must be in the android: namespace) + * @param defaultValue the default value to use if the value is not set + * @return the attribute boolean value + */ + private static boolean getBoolean(INode node, String attribute, boolean defaultValue) { + String valueString = node.getStringAttr(ANDROID_URI, attribute); + if (valueString != null) { + return Boolean.valueOf(valueString); + } + + return defaultValue; + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java index 0b1d9e6..39b521b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java @@ -196,6 +196,8 @@ class DependencyGraph { // Cycle - what do we do to highlight this? List<Constraint> path = getPathTo(start.node, view.node, vertical); if (path != null) { + // TODO: display to the user somehow. We need log access for the + // view rules. System.out.println(Constraint.describePath(path, null, null)); } } else { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelinePainter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelinePainter.java index be0fb09..8299be3 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelinePainter.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelinePainter.java @@ -83,6 +83,12 @@ public final class GuidelinePainter implements IFeedbackPainter { } } gc.drawRect(state.mBounds); + + // Draw baseline preview too + if (feedback.dragBaseline != -1) { + int y = state.mBounds.y + feedback.dragBaseline; + gc.drawLine(state.mBounds.x, y, state.mBounds.x2(), y); + } } List<String> strings = new ArrayList<String>(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/removecol.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/removecol.png Binary files differnew file mode 100644 index 0000000..c41261a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/removecol.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/showgrid.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/showgrid.png Binary files differnew file mode 100644 index 0000000..6f7bf91 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/showgrid.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/snap.png b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/snap.png Binary files differnew file mode 100644 index 0000000..b50a16e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/snap.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java index f28c681..38ab6d8 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java @@ -435,7 +435,7 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor { } UiAttributeNode currAttrNode = null; - for (UiAttributeNode attrNode : currentUiNode.getUiAttributes()) { + for (UiAttributeNode attrNode : currentUiNode.getAllUiAttributes()) { if (attrNode.getDescriptor().getXmlLocalName().equals(attrName)) { currAttrNode = attrNode; break; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java index 62cd172..d4b0a67 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java @@ -16,6 +16,7 @@ package com.android.ide.eclipse.adt.internal.editors.descriptors; +import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT; @@ -25,11 +26,13 @@ import static com.android.ide.common.layout.LayoutConstants.EDIT_TEXT; import static com.android.ide.common.layout.LayoutConstants.EXPANDABLE_LIST_VIEW; import static com.android.ide.common.layout.LayoutConstants.FQCN_ADAPTER_VIEW; import static com.android.ide.common.layout.LayoutConstants.GALLERY; +import static com.android.ide.common.layout.LayoutConstants.GRID_LAYOUT; import static com.android.ide.common.layout.LayoutConstants.GRID_VIEW; import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX; import static com.android.ide.common.layout.LayoutConstants.LIST_VIEW; import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; 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.VALUE_FILL_PARENT; import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.REQUEST_FOCUS; @@ -47,8 +50,8 @@ import org.eclipse.swt.graphics.Image; import java.util.ArrayList; import java.util.HashSet; import java.util.Map; -import java.util.Set; import java.util.Map.Entry; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -683,23 +686,28 @@ public final class DescriptorsUtils { // both W/H. Otherwise default to wrap_layout. ElementDescriptor descriptor = node.getDescriptor(); - if (descriptor.getXmlLocalName().equals(REQUEST_FOCUS)) { - // Don't add ids etc to <requestFocus> + String name = descriptor.getXmlLocalName(); + if (name.equals(REQUEST_FOCUS) || name.equals(SPACE)) { + // Don't add ids etc to <requestFocus>, or to grid spacers return; } - boolean fill = descriptor.hasChildren() && - node.getUiParent() instanceof UiDocumentNode; - node.setAttributeValue( - ATTR_LAYOUT_WIDTH, - SdkConstants.NS_RESOURCES, - fill ? VALUE_FILL_PARENT : VALUE_WRAP_CONTENT, - false /* override */); - node.setAttributeValue( - ATTR_LAYOUT_HEIGHT, - SdkConstants.NS_RESOURCES, - fill ? VALUE_FILL_PARENT : VALUE_WRAP_CONTENT, - false /* override */); + // Width and height are mandatory in all layouts except GridLayout + boolean setSize = !node.getUiParent().getDescriptor().getXmlName().equals(GRID_LAYOUT); + if (setSize) { + boolean fill = descriptor.hasChildren() && + node.getUiParent() instanceof UiDocumentNode; + node.setAttributeValue( + ATTR_LAYOUT_WIDTH, + SdkConstants.NS_RESOURCES, + fill ? VALUE_FILL_PARENT : VALUE_WRAP_CONTENT, + false /* override */); + node.setAttributeValue( + ATTR_LAYOUT_HEIGHT, + SdkConstants.NS_RESOURCES, + fill ? VALUE_FILL_PARENT : VALUE_WRAP_CONTENT, + false /* override */); + } String freeId = getFreeWidgetId(node); if (freeId != null) { @@ -710,8 +718,11 @@ public final class DescriptorsUtils { false /* override */); } - // Don't set default text value into edit texts - they typically start out blank - if (!descriptor.getXmlLocalName().equals(EDIT_TEXT)) { + // Set a text attribute on textual widgets -- but only on those that define a text + // attribute + if (descriptor.definesAttribute(ANDROID_URI, ATTR_TEXT) + // Don't set default text value into edit texts - they typically start out blank + && !descriptor.getXmlLocalName().equals(EDIT_TEXT)) { String type = getBasename(descriptor.getUiName()); node.setAttributeValue( ATTR_TEXT, diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java index e0f6959..ce3d59a 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java @@ -446,6 +446,24 @@ public class ElementDescriptor implements Comparable<ElementDescriptor> { return new String(c).replace("-", " "); //$NON-NLS-1$ //$NON-NLS-2$ } + /** + * Returns true if this node defines the given attribute + * + * @param namespaceUri the namespace URI of the target attribute + * @param attributeName the attribute name + * @return true if this element defines an attribute of the given name and namespace + */ + public boolean definesAttribute(String namespaceUri, String attributeName) { + for (AttributeDescriptor desc : mAttributes) { + if (desc.getXmlLocalName().equals(attributeName) && + desc.getNamespaceUri().equals(namespaceUri)) { + return true; + } + } + + return false; + } + // Implements Comparable<ElementDescriptor>: public int compareTo(ElementDescriptor o) { return mUiName.compareToIgnoreCase(o.mUiName); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ProjectCallback.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ProjectCallback.java index c71005c..fec6162 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ProjectCallback.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ProjectCallback.java @@ -20,6 +20,7 @@ import static com.android.ide.common.layout.LayoutConstants.ANDROID_PKG_PREFIX; import static com.android.ide.common.layout.LayoutConstants.CALENDAR_VIEW; import static com.android.ide.common.layout.LayoutConstants.EXPANDABLE_LIST_VIEW; import static com.android.ide.common.layout.LayoutConstants.FQCN_GRID_VIEW; +import static com.android.ide.common.layout.LayoutConstants.FQCN_SPINNER; import static com.android.ide.common.layout.LayoutConstants.GRID_VIEW; import static com.android.ide.common.layout.LayoutConstants.LIST_VIEW; import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_FRAGMENT; @@ -499,7 +500,9 @@ public final class ProjectCallback extends LegacyCallback { if (fqcn.endsWith(LIST_VIEW)) { // including EXPANDABLE_LIST_VIEW return fqcn; } else if (fqcn.equals(FQCN_GRID_VIEW)) { - return fqcn; + return fqcn; + } else if (fqcn.equals(FQCN_SPINNER)) { + return fqcn; } else if (fqcn.startsWith(ANDROID_PKG_PREFIX)) { return null; } @@ -578,6 +581,9 @@ public final class ProjectCallback extends LegacyCallback { if (listFqcn.endsWith(EXPANDABLE_LIST_VIEW)) { binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_EXPANDABLE_LIST_ITEM, true /* isFramework */, 1)); + } else if (listFqcn.equals(FQCN_SPINNER)) { + binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_SPINNER_ITEM, + true /* isFramework */, 1)); } else { binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_LIST_ITEM, true /* isFramework */, 1)); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java index 07593cf..e8e3c9a 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java @@ -225,7 +225,7 @@ public class UiElementPullParser extends BasePullParser { UiElementNode node = getCurrentNode(); if (node != null) { - Collection<UiAttributeNode> attributes = node.getUiAttributes(); + Collection<UiAttributeNode> attributes = node.getAllUiAttributes(); int count = attributes.size(); return count + (mZeroAttributeIsPadding ? 1 : 0); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java index 8212877..42c5344 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java @@ -44,9 +44,9 @@ import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice; +import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice.DeviceConfig; import com.android.ide.eclipse.adt.internal.sdk.LayoutDeviceManager; import com.android.ide.eclipse.adt.internal.sdk.Sdk; -import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice.DeviceConfig; import com.android.resources.Density; import com.android.resources.NightMode; import com.android.resources.ResourceFolderType; @@ -1080,11 +1080,11 @@ public class ConfigurationComposite extends Composite { } private ConfigMatch selectConfigMatch(List<ConfigMatch> matches) { - // API 11+ : look for a x-large device. - if (mProjectTarget.getVersion().getApiLevel() >= 11) { - // TODO: Revisit this once phones can run higher APIs. - // Maybe check the compatible-screen tag in the manifest to figure out what kind of - // device should be used for display. + // API 11-12: look for a x-large device + int apiLevel = mProjectTarget.getVersion().getApiLevel(); + if (apiLevel >= 11 && apiLevel < 13) { + // TODO: Maybe check the compatible-screen tag in the manifest to figure out + // what kind of device should be used for display. Collections.sort(matches, new TabletConfigComparator()); } else { // lets look for a high density device diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java index c37ffe8..dbb8dac 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java @@ -16,11 +16,13 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import static com.android.ide.common.layout.LayoutConstants.FQCN_SPACE; import static com.android.ide.common.layout.LayoutConstants.GESTURE_OVERLAY_VIEW; import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_MERGE; 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.common.rendering.api.Capability; import com.android.ide.common.rendering.api.MergeCookie; import com.android.ide.common.rendering.api.ViewInfo; @@ -203,8 +205,9 @@ public class CanvasViewInfo implements IPropertySource { /** * Returns all the children of the canvas view info where each child corresponds to a - * unique node. This is intended for use by the outline for example, where only the - * actual nodes are displayed, not the views themselves. + * unique node that the user can see and select. This is intended for use by the + * outline for example, where only the actual nodes are displayed, not the views + * themselves. * <p> * Most views have their own nodes, so this is generally the same as * {@link #getChildren}, except in the case where you for example include a view that @@ -215,6 +218,8 @@ public class CanvasViewInfo implements IPropertySource { * never null */ public List<CanvasViewInfo> getUniqueChildren() { + boolean haveHidden = false; + for (CanvasViewInfo info : mChildren) { if (info.mNodeSiblings != null) { // We have secondary children; must create a new collection containing @@ -229,6 +234,19 @@ public class CanvasViewInfo implements IPropertySource { } return children; } + + haveHidden |= info.isHidden(); + } + + if (haveHidden) { + List<CanvasViewInfo> children = new ArrayList<CanvasViewInfo>(mChildren.size()); + for (CanvasViewInfo vi : mChildren) { + if (!vi.isHidden()) { + children.add(vi); + } + } + + return children; } return mChildren; @@ -260,6 +278,7 @@ public class CanvasViewInfo implements IPropertySource { * Returns the name of the {@link CanvasViewInfo}. * Could be null, although unlikely. * Experience shows this is the full qualified Java name of the View. + * TODO: Rename this method to getFqcn. * * @return the name of the view info, or null * @@ -407,6 +426,11 @@ public class CanvasViewInfo implements IPropertySource { * @return True if this is a tiny layout or invisible view */ public boolean isInvisible() { + if (isHidden()) { + // Don't expand and highlight hidden widgets + return false; + } + if (mAbsRect.width < SELECTION_MIN_SIZE || mAbsRect.height < SELECTION_MIN_SIZE) { return mUiViewNode != null && (mUiViewNode.getDescriptor().hasChildren() || mAbsRect.width <= 0 || mAbsRect.height <= 0); @@ -416,6 +440,21 @@ public class CanvasViewInfo implements IPropertySource { } /** + * Returns true if this {@link CanvasViewInfo} represents a widget that should be + * hidden, such as a {@code <Space>} which are typically not manipulated by the user + * through dragging etc. + * + * @return true if this is a hidden view + */ + public boolean isHidden() { + if (GridLayoutRule.sDebugGridLayout) { + return false; + } + + return FQCN_SPACE.equals(mName); + } + + /** * Is this {@link CanvasViewInfo} a view that has had its padding inflated in order to * make it visible during selection or dragging? Note that this is NOT considered to * be the case in the explode-all-views mode where all nodes have their padding @@ -462,7 +501,7 @@ public class CanvasViewInfo implements IPropertySource { SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds); - for (UiAttributeNode attr : uiNode.getUiAttributes()) { + for (UiAttributeNode attr : uiNode.getAllUiAttributes()) { String value = attr.getCurrentValue(); if (value != null && value.length() > 0) { AttributeDescriptor attrDesc = attr.getDescriptor(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java index 89b9f8e..f3582ec 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java @@ -16,6 +16,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import com.android.ide.common.api.IDragElement; +import com.android.ide.common.api.INode; import com.android.ide.common.api.IDragElement.IDragAttribute; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; @@ -23,6 +24,7 @@ import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDe import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; @@ -36,7 +38,10 @@ import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.dnd.TransferData; import org.eclipse.swt.widgets.Composite; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * The {@link ClipboardSupport} class manages the native clipboard, providing operations @@ -199,6 +204,30 @@ public class ClipboardSupport { // resetting the selection. mCanvas.getLayoutEditor().wrapUndoEditXmlModel(title, new Runnable() { public void run() { + // Segment the deleted nodes into clusters of siblings + Map<NodeProxy, List<INode>> clusters = + new HashMap<NodeProxy, List<INode>>(); + for (SelectionItem cs : selection) { + NodeProxy node = cs.getNode(); + INode parent = node.getParent(); + List<INode> children = clusters.get(parent); + if (children == null) { + children = new ArrayList<INode>(); + clusters.put((NodeProxy) parent, children); + } + children.add(node); + } + + // Notify parent views about children getting deleted + RulesEngine rulesEngine = mCanvas.getRulesEngine(); + LayoutEditor editor = mCanvas.getLayoutEditor(); + for (Map.Entry<NodeProxy, List<INode>> entry : clusters.entrySet()) { + NodeProxy parent = entry.getKey(); + List<INode> children = entry.getValue(); + assert children != null && children.size() > 0; + rulesEngine.callOnRemovingChildren(editor, parent, children); + } + for (SelectionItem cs : selection) { CanvasViewInfo vi = cs.getViewInfo(); // You can't delete the root element diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java index ba09220..59b9602 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java @@ -22,6 +22,7 @@ import org.eclipse.swt.dnd.DropTargetEvent; import org.eclipse.swt.events.MenuDetectEvent; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.graphics.Point; /** * A {@link ControlPoint} is a coordinate in the canvas control which corresponds @@ -182,4 +183,13 @@ public final class ControlPoint { } return true; } + + /** + * Returns this point as an SWT point in the display coordinate system + * + * @return this point as an SWT point in the display coordinate system + */ + public Point toDisplayPoint() { + return mCanvas.toDisplay(x, y); + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java index 90b95ee..e8cd418 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java @@ -20,6 +20,7 @@ import static com.android.ide.common.layout.LayoutConstants.EXPANDABLE_LIST_VIEW import static com.android.ide.common.layout.LayoutConstants.FQCN_GESTURE_OVERLAY_VIEW; import static com.android.ide.common.layout.LayoutConstants.GRID_VIEW; import static com.android.ide.common.layout.LayoutConstants.LIST_VIEW; +import static com.android.ide.common.layout.LayoutConstants.SPINNER; import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_FRAGMENT; import com.android.ide.common.api.IMenuCallback; @@ -49,8 +50,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.TreeMap; import java.util.Map.Entry; +import java.util.TreeMap; import java.util.regex.Pattern; /** @@ -248,9 +249,11 @@ import java.util.regex.Pattern; UiViewElementNode node = item.getViewInfo().getUiViewNode(); String name = node.getDescriptor().getXmlLocalName(); boolean isGrid = name.equals(GRID_VIEW); - if (name.equals(LIST_VIEW) || name.equals(EXPANDABLE_LIST_VIEW) || isGrid) { + boolean isSpinner = name.equals(SPINNER); + if (name.equals(LIST_VIEW) || name.equals(EXPANDABLE_LIST_VIEW) + || isGrid || isSpinner) { mMenuManager.insertBefore(endId, new Separator()); - mMenuManager.insertBefore(endId, new ListViewTypeMenu(mCanvas, isGrid)); + mMenuManager.insertBefore(endId, new ListViewTypeMenu(mCanvas, isGrid, isSpinner)); return; } else if (name.equals(VIEW_FRAGMENT) && selection.size() == 1) { mMenuManager.insertBefore(endId, new Separator()); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java index cd4105d..b1366a3 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java @@ -488,6 +488,17 @@ public class GCWrapper implements IGraphics { return color; } + // dots + + public void drawPoint(int x, int y) { + checkGC(); + useStrokeAlpha(); + x = mHScale.translate(x); + y = mVScale.translate(y); + + getGc().drawPoint(x, y); + } + // arrows private static final int MIN_LENGTH = 10; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java index 16577c1..cab569f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java @@ -16,6 +16,8 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import com.android.util.Pair; + import org.eclipse.swt.events.KeyEvent; import java.util.Collections; @@ -139,4 +141,16 @@ public abstract class Gesture { public boolean keyReleased(KeyEvent event) { return false; } + + /** + * Returns whether tooltips should be display below and to the right of the mouse + * cursor. + * + * @return a pair of booleans, the first indicating whether the tooltip should be + * below and the second indicating whether the tooltip should be displayed to + * the right of the mouse cursor. + */ + public Pair<Boolean, Boolean> getTooltipPosition() { + return Pair.of(true, true); + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java index 62d5dcd..8c4bf15 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java @@ -70,6 +70,9 @@ public class GestureManager { /** A listener for drag source events. */ private final DragSourceListener mDragSourceListener = new CanvasDragSourceListener(); + /** Tooltip shown during the gesture, or null */ + private GestureToolTip mTooltip; + /** * The list of overlays associated with {@link #mCurrentGesture}. Will be * null before it has been initialized lazily by the paint routine (the @@ -363,7 +366,6 @@ public class GestureManager { /** * Update the Eclipse status message with any feedback messages from the given * {@link DropFeedback} object, or clean up if there is no more feedback to process - * * @param feedback the feedback whose message we want to display, or null to clear the * message if previously set */ @@ -393,6 +395,18 @@ public class GestureManager { status.setMessage(null); status.setErrorMessage(null); } + + // Tooltip + if (feedback != null && feedback.tooltip != null) { + if (mTooltip == null) { + Pair<Boolean,Boolean> position = mCurrentGesture.getTooltipPosition(); + mTooltip = new GestureToolTip(mCanvas, position.getFirst(), position.getSecond()); + } + mTooltip.update(feedback.tooltip); + } else if (mTooltip != null) { + mTooltip.dispose(); + mTooltip = null; + } } /** @@ -706,7 +720,7 @@ public class GestureManager { if (!insideSelection) { CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p); - if (vi != null && !vi.isRoot()) { + if (vi != null && !vi.isRoot() && !vi.isHidden()) { selectionManager.selectSingle(vi); insideSelection = true; } @@ -726,7 +740,7 @@ public class GestureManager { } else { // Only drag non-root items. for (SelectionItem cs : selections) { - if (!cs.isRoot()) { + if (!cs.isRoot() && !cs.isHidden()) { mDragSelection.add(cs); } } @@ -737,7 +751,7 @@ public class GestureManager { // If you are dragging a non-selected item, select it if (mDragSelection.isEmpty()) { CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p); - if (vi != null && !vi.isRoot()) { + if (vi != null && !vi.isRoot() && !vi.isHidden()) { selectionManager.selectSingle(vi); mDragSelection.addAll(selections); } @@ -761,7 +775,8 @@ public class GestureManager { // If you drag on the -background-, we make that into a marquee // selection - if (!e.doit || (imageCount == 1 && mDragSelection.get(0).isRoot())) { + if (!e.doit || (imageCount == 1 + && (mDragSelection.get(0).isRoot() || mDragSelection.get(0).isHidden()))) { boolean toggle = (mLastStateMask & (SWT.CTRL | SWT.SHIFT | SWT.COMMAND)) != 0; startGesture(controlPoint, new MarqueeGesture(mCanvas, toggle), mLastStateMask); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureToolTip.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureToolTip.java new file mode 100644 index 0000000..7d71ec7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureToolTip.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.layout.gle2; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CLabel; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +/** + * A dedicated tooltip used during gestures, for example to show the resize dimensions. + * <p> + * This is necessary because {@link org.eclipse.jface.window.ToolTip} causes flicker when + * used to dynamically update the position and text of the tip, and it does not seem to + * have setter methods to update the text or position without recreating the tip. + */ +public class GestureToolTip { + /** + * The alpha to use for the tooltip window (which sadly will apply to the tooltip text + * as well.) + */ + private static final int SHELL_TRANSPARENCY = 220; + + /** The size of the font displayed in the tooltip */ + private static final int FONT_SIZE = 9; + + /** Horizontal delta from the mouse cursor to shift the tooltip by */ + private static final int OFFSET_X = 20; + + /** Vertical delta from the mouse cursor to shift the tooltip by */ + private static final int OFFSET_Y = 20; + + /** The label which displays the tooltip */ + private CLabel mLabel; + + /** The shell holding the tooltip */ + private Shell mShell; + + /** The font shown in the label; held here such that it can be disposed of after use */ + private Font mFont; + + /** Should the tooltip be displayed below the cursor? */ + private boolean mBelow; + + /** Should the tooltip be displayed to the right of the cursor? */ + private boolean mToRightOf; + + /** + * Creates a new tooltip over the given parent with the given relative position. + * + * @param parent the parent control + * @param below if true, display the tooltip below the mouse cursor otherwise above + * @param toRightOf if true, display the tooltip to the right of the mouse cursor, + * otherwise to the left + */ + public GestureToolTip(Composite parent, boolean below, boolean toRightOf) { + mBelow = below; + mToRightOf = toRightOf; + + mShell = new Shell(parent.getShell(), SWT.ON_TOP | SWT.TOOL | SWT.NO_FOCUS); + mShell.setLayout(new FillLayout()); + mShell.setAlpha(SHELL_TRANSPARENCY); + + Display display = parent.getDisplay(); + mLabel = new CLabel(mShell, SWT.SHADOW_NONE); + mLabel.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + mLabel.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + + Font systemFont = display.getSystemFont(); + FontData[] fd = systemFont.getFontData(); + for (int i = 0; i < fd.length; i++) { + fd[i].setHeight(FONT_SIZE); + } + mFont = new Font(display, fd); + mLabel.setFont(mFont); + + mShell.setVisible(false); + + } + + /** + * Show the tooltip at the given position and with the given text + * + * @param text the new text to be displayed + */ + public void update(String text) { + Point location = mShell.getDisplay().getCursorLocation(); + + mLabel.setText(text); + + // Pack the label to its minimum size -- unless we are positioning the tooltip + // on the left. Because of the way SWT works (at least on the OSX) this sometimes + // creates flicker, because when we switch to a longer string (such as when + // switching from "52dp" to "wrap_content" during a resize) the window size will + // change first, and then the location will update later - so there will be a + // brief flash of the longer label before it is moved to the right position on the + // left. To work around this, we simply pass false to pack such that it will reuse + // its cached size, which in practice means that for labels on the right, the + // label will grow but not shrink. + // This workaround is disabled because it doesn't work well in Eclipse 3.5; the + // labels don't grow when they should. Re-enable when we drop 3.5 support. + //boolean changed = mToRightOf; + boolean changed = true; + + mShell.pack(changed); + Point size = mShell.getSize(); + + if (mBelow) { + location.y += OFFSET_Y; + } else { + location.y -= OFFSET_Y; + location.y -= size.y; + } + + if (mToRightOf) { + location.x += OFFSET_X; + } else { + location.x -= OFFSET_X; + location.x -= size.x; + } + + mShell.setLocation(location); + + if (!mShell.isVisible()) { + mShell.setVisible(true); + } + } + + /** Hide the tooltip and dispose of any associated resources */ + public void dispose() { + mShell.dispose(); + mFont.dispose(); + + mShell = null; + mFont = null; + mLabel = null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java index 1d36f7b..32cc45d 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java @@ -17,6 +17,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import com.android.ide.common.api.INode; +import com.android.ide.common.api.Margins; import com.android.ide.common.api.Point; import com.android.ide.common.layout.LayoutConstants; import com.android.ide.common.rendering.api.Capability; @@ -30,9 +31,11 @@ import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewEleme import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory; import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.resources.Density; import com.android.sdklib.SdkConstants; import org.eclipse.core.filesystem.EFS; @@ -83,8 +86,8 @@ import org.eclipse.ui.IEditorSite; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.PartInitException; import org.eclipse.ui.actions.ActionFactory; -import org.eclipse.ui.actions.ContributionItemFactory; import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction; +import org.eclipse.ui.actions.ContributionItemFactory; import org.eclipse.ui.ide.IDE; import org.eclipse.ui.internal.ide.IDEWorkbenchMessages; import org.eclipse.ui.texteditor.ITextEditor; @@ -589,7 +592,7 @@ public class LayoutCanvas extends Canvas { redraw(); } - /* package */ double getScale() { + public double getScale() { return mHScale.getScale(); } @@ -845,7 +848,8 @@ public class LayoutCanvas extends Canvas { CanvasViewInfo vi = mViewHierarchy.findViewInfoAt(p); // We don't hover on the root since it's not a widget per see and it is always there. - if (vi != null && vi.isRoot()) { + // We also skip spacers... + if (vi != null && (vi.isRoot() || vi.isHidden())) { vi = null; } @@ -1335,6 +1339,25 @@ public class LayoutCanvas extends Canvas { }); } + /** + * Returns the insets associated with views of the given fully qualified name, for the + * current theme and screen type. + * + * @param fqcn the fully qualified name to the widget type + * @return the insets, or null if unknown + */ + public Margins getInsets(String fqcn) { + if (ViewMetadataRepository.INSETS_SUPPORTED) { + ConfigurationComposite configComposite = + mLayoutEditor.getGraphicalEditor().getConfigurationComposite(); + String theme = configComposite.getTheme(); + Density density = configComposite.getDensity(); + return ViewMetadataRepository.getInsets(fqcn, density, theme); + } else { + return null; + } + } + private void debugPrintf(String message, Object... params) { if (DEBUG) { AdtPlugin.printToConsole("Canvas", String.format(message, params)); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java index 906a2ec..0024bf3 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java @@ -49,6 +49,8 @@ public class LayoutMetadata { public static final String DEFAULT_EXPANDABLE_LIST_ITEM = "simple_expandable_list_item_2"; //$NON-NLS-1$ /** The default layout to use for list items in plain list views */ public static final String DEFAULT_LIST_ITEM = "simple_list_item_2"; //$NON-NLS-1$ + /** The default layout to use for list items in spinners */ + public static final String DEFAULT_SPINNER_ITEM = "simple_spinner_item"; //$NON-NLS-1$ /** The string to start metadata comments with */ private static final String COMMENT_PROLOGUE = " Preview: "; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java index c1c5e5a..4f2feb7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java @@ -50,17 +50,22 @@ public class ListViewTypeMenu extends SubmenuAction { private final LayoutCanvas mCanvas; /** When true, this menu is for a grid rather than a simple list */ private boolean mGrid; + /** When true, this menu is for a spinner rather than a simple list */ + private boolean mSpinner; /** * Creates a "Preview List Content" menu * * @param canvas associated canvas * @param isGrid whether the menu is for a grid rather than a list + * @param isSpinner whether the menu is for a spinner rather than a list */ - public ListViewTypeMenu(LayoutCanvas canvas, boolean isGrid) { - super(isGrid ? "Preview Grid Content" : "Preview List Content"); + public ListViewTypeMenu(LayoutCanvas canvas, boolean isGrid, boolean isSpinner) { + super(isGrid ? "Preview Grid Content" : isSpinner ? "Preview Spinner Layout" + : "Preview List Content"); mCanvas = canvas; mGrid = isGrid; + mSpinner = isSpinner; } @Override @@ -77,6 +82,17 @@ public class ListViewTypeMenu extends SubmenuAction { selected = selected.substring(ANDROID_LAYOUT_PREFIX.length()); } } + + if (mSpinner) { + action = new SetListTypeAction("Spinner Item", + "simple_spinner_item", selected); //$NON-NLS-1$ + new ActionContributionItem(action).fill(menu, -1); + action = new SetListTypeAction("Spinner Dropdown Item", + "simple_spinner_dropdown_item", selected); //$NON-NLS-1$ + new ActionContributionItem(action).fill(menu, -1); + return; + } + action = new SetListTypeAction("Simple List Item", "simple_list_item_1", selected); //$NON-NLS-1$ new ActionContributionItem(action).fill(menu, -1); 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 9c27f6e..d29ee45 100755 --- 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 @@ -17,6 +17,10 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; +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.ATTR_LAYOUT_ROW_SPAN; import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION; import static com.android.ide.common.layout.LayoutConstants.ATTR_SRC; import static com.android.ide.common.layout.LayoutConstants.ATTR_TEXT; @@ -30,6 +34,7 @@ import com.android.annotations.VisibleForTesting; import com.android.ide.common.api.INode; import com.android.ide.common.api.InsertType; 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.IconFactory; import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; @@ -520,6 +525,42 @@ public class OutlinePage extends ContentOutlinePage Node xmlNode = node.getXmlNode(); if (xmlNode instanceof Element) { Element e = (Element) xmlNode; + + // Temporary diagnostics code when developing GridLayout + if (GridLayoutRule.sDebugGridLayout && e.getParentNode() != null + && e.getParentNode().getNodeName() != null) { + if (e.getParentNode().getNodeName().equals("GridLayout")) { //$NON-NLS-1$ + // Attach row/column info + styledString.append(" - cell (", QUALIFIER_STYLER); + String row = e.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_ROW); + if (row.length() == 0) { + row = "?"; + } + String column = e.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_COLUMN); + if (column.length() == 0) { + column = "?"; + } + String rowSpan = e.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_ROW_SPAN); + String columnSpan = e.getAttributeNS(ANDROID_URI, + ATTR_LAYOUT_COLUMN_SPAN); + if (rowSpan.length() == 0) { + rowSpan = "1"; + } + if (columnSpan.length() == 0) { + columnSpan = "1"; + } + + styledString.append(row, QUALIFIER_STYLER); + styledString.append(',', QUALIFIER_STYLER); + styledString.append(column, QUALIFIER_STYLER); + styledString.append("), span=(", QUALIFIER_STYLER); + styledString.append(columnSpan, QUALIFIER_STYLER); + styledString.append(',', QUALIFIER_STYLER); + styledString.append(rowSpan, QUALIFIER_STYLER); + styledString.append(')', QUALIFIER_STYLER); + } + } + if (e.hasAttributeNS(ANDROID_URI, ATTR_TEXT)) { // Show the text attribute String text = e.getAttributeNS(ANDROID_URI, ATTR_TEXT); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java index 4b6cab1..4ee3aac 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java @@ -743,12 +743,13 @@ public class PaletteControl extends Composite { int height = mImageLayoutBounds.height; assert mImageLayoutBounds.x == 0; assert mImageLayoutBounds.y == 0; - double scale = mEditor.getCanvasControl().getScale(); - - int x = (int) (-scale * width / 2); - int y = (int) (-scale * height / 2); bounds = new Rect(0, 0, width, height); - dragBounds = new Rect(x, y, width, height); + double scale = mEditor.getCanvasControl().getScale(); + int scaledWidth = (int) (scale * width); + int scaledHeight = (int) (scale * height); + int x = -scaledWidth / 2; + int y = -scaledHeight / 2; + dragBounds = new Rect(x, y, scaledWidth, scaledHeight); } SimpleElement se = new SimpleElement( diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java index 4b743b4..fc3d7d7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java @@ -29,8 +29,8 @@ import com.android.ide.common.rendering.api.RenderSession; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.rendering.api.Result; import com.android.ide.common.rendering.api.SessionParams; -import com.android.ide.common.rendering.api.ViewInfo; import com.android.ide.common.rendering.api.SessionParams.RenderingMode; +import com.android.ide.common.rendering.api.ViewInfo; import com.android.ide.common.resources.ResourceResolver; import com.android.ide.common.resources.configuration.ScreenSizeQualifier; import com.android.ide.eclipse.adt.AdtPlugin; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java index 1714828..ec6c7c6 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java @@ -23,6 +23,7 @@ import com.android.ide.common.api.SegmentType; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.Position; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; +import com.android.util.Pair; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.graphics.GC; @@ -79,6 +80,7 @@ public class ResizeGesture extends Gesture { Rect newBounds = getNewBounds(pos); mFeedback = rulesEngine.callOnResizeBegin(mChildNode, mParentNode, newBounds, mHorizontalEdge, mVerticalEdge); + update(pos); mCanvas.getGestureManager().updateMessage(mFeedback); } @@ -120,6 +122,11 @@ public class ResizeGesture extends Gesture { mCanvas.getSelectionOverlay().setHidden(false); } + @Override + public Pair<Boolean, Boolean> getTooltipPosition() { + return Pair.of(mHorizontalEdge != SegmentType.TOP, mVerticalEdge != SegmentType.LEFT); + } + /** * For the new mouse position, compute the resized bounds (the bounding rectangle that * the view should be resized to). This is not just a width or height, since in some diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandles.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandles.java index 9b10b6e..07285ea 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandles.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandles.java @@ -16,6 +16,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import com.android.ide.common.api.Margins; import com.android.ide.common.api.Rect; import com.android.ide.common.api.ResizePolicy; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.Position; @@ -43,7 +44,7 @@ public class SelectionHandles implements Iterable<SelectionHandle> { public SelectionHandles(SelectionItem item) { mItem = item; - createHandles(); + createHandles(item.getCanvas()); } /** @@ -69,7 +70,7 @@ public class SelectionHandles implements Iterable<SelectionHandle> { * Create the {@link SelectionHandle} objects for the selection item, according to its * {@link ResizePolicy}. */ - private void createHandles() { + private void createHandles(LayoutCanvas canvas) { NodeProxy selectedNode = mItem.getNode(); Rect r = selectedNode.getBounds(); if (!r.isValid()) { @@ -88,9 +89,17 @@ public class SelectionHandles implements Iterable<SelectionHandle> { int y1 = r.y; int w = r.w; int h = r.h; - int x2 = x1 + w; int y2 = y1 + h; + + Margins insets = canvas.getInsets(mItem.getNode().getFqcn()); + if (insets != null) { + x1 += insets.left; + x2 -= insets.right; + y1 += insets.top; + y2 -= insets.bottom; + } + int mx = (x1 + x2) / 2; int my = (y1 + y2) / 2; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java index 4afb123..fcdf79a 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java @@ -75,6 +75,7 @@ class SelectionItem { /** * Returns true when this selection item represents the root, the top level * layout element in the editor. + * * @return True if and only if this element is at the root of the hierarchy */ public boolean isRoot() { @@ -82,6 +83,16 @@ class SelectionItem { } /** + * Returns true if this item represents a widget that should not be manipulated by the + * user. + * + * @return True if this widget should not be manipulated directly by the user + */ + public boolean isHidden() { + return mCanvasViewInfo.isHidden(); + } + + /** * Returns the selected view info. Cannot be null. * * @return the selected view info. Cannot be null. @@ -104,6 +115,11 @@ class SelectionItem { return mNodeProxy; } + /** Returns the canvas associated with this selection (never null) */ + LayoutCanvas getCanvas() { + return mCanvas; + } + //---- /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java index aeb05a2..757e2c4 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java @@ -15,10 +15,12 @@ */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import static com.android.ide.common.layout.LayoutConstants.FQCN_SPACE; import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_MARGIN; import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_RADIUS; import com.android.ide.common.api.INode; +import com.android.ide.common.layout.GridLayoutRule; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; @@ -335,6 +337,10 @@ public class SelectionManager implements ISelectionProvider { CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p); + if (vi != null && vi.isHidden()) { + vi = vi.getParent(); + } + if (isMultiClick && !isCycleClick) { // Case where shift is pressed: pointed object is toggled. @@ -551,6 +557,9 @@ public class SelectionManager implements ISelectionProvider { mSelections.clear(); for (CanvasViewInfo viewInfo : viewInfos) { + if (viewInfo.isHidden()) { + continue; + } mSelections.add(createSelection(viewInfo)); } @@ -814,6 +823,15 @@ public class SelectionManager implements ISelectionProvider { for (INode node : nodes) { CanvasViewInfo viewInfo = mCanvas.getViewHierarchy().findViewInfoFor(node); if (viewInfo != null) { + if (nodes.size() > 1 && viewInfo.isHidden()) { + // Skip spacers - unless you're dropping just one + continue; + } + if (GridLayoutRule.sDebugGridLayout && viewInfo.getName().equals(FQCN_SPACE)) { + // In debug mode they might not be marked as hidden but we never never + // want to select these guys + continue; + } newChildren.add(viewInfo); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java index b3cc13b..0186212 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java @@ -19,6 +19,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import com.android.ide.common.api.DrawingStyle; import com.android.ide.common.api.IGraphics; import com.android.ide.common.api.INode; +import com.android.ide.common.api.Margins; import com.android.ide.common.api.Rect; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; @@ -189,7 +190,21 @@ public class SelectionOverlay extends Overlay { } gc.useStyle(DrawingStyle.SELECTION); - gc.drawRect(r); + + Margins insets = mCanvas.getInsets(selectedNode.getFqcn()); + int x1 = r.x; + int y1 = r.y; + int x2 = r.x2() + 1; + int y2 = r.y2() + 1; + + if (insets != null) { + x1 += insets.left; + x2 -= insets.right; + y1 += insets.top; + y2 -= insets.bottom; + } + + gc.drawRect(x1, y1, x2, y2); // Paint sibling rectangles, if applicable CanvasViewInfo view = item.getViewInfo(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java index 90e6228..93a3328 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java @@ -52,6 +52,16 @@ public enum SwtDrawingStyle { GUIDELINE_DASHED(new RGB(0x00, 0xAA, 0x00), 192, SWT.LINE_CUSTOM), /** + * The style definition corresponding to {@link DrawingStyle#DISTANCE} + */ + DISTANCE(new RGB(0xFF, 0x00, 0x00), 192 - 32, SWT.LINE_SOLID), + + /** + * The style definition corresponding to {@link DrawingStyle#GRID} + */ + GRID(new RGB(0xAA, 0xAA, 0xAA), 128, SWT.LINE_SOLID), + + /** * The style definition corresponding to {@link DrawingStyle#HOVER} */ HOVER(null, 0, new RGB(0xFF, 0xFF, 0xFF), 40, 1, SWT.LINE_DOT), @@ -242,6 +252,10 @@ public enum SwtDrawingStyle { return GUIDELINE_SHADOW; case GUIDELINE_DASHED: return GUIDELINE_DASHED; + case DISTANCE: + return DISTANCE; + case GRID: + return GRID; case HOVER: return HOVER; case HOVER_SELECTION: 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 e23f05c..d213646 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 @@ -24,6 +24,7 @@ import com.android.ide.common.api.INode; import com.android.ide.common.api.IValidator; import com.android.ide.common.api.IViewMetadata; import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.Margins; import com.android.ide.common.api.Rect; import com.android.ide.common.resources.ResourceRepository; import com.android.ide.eclipse.adt.AdtPlugin; @@ -163,6 +164,10 @@ class ClientRulesEngine implements IClientRulesEngine { public FillPreference getFillPreference() { return ViewMetadataRepository.get().getFillPreference(fqcn); } + + public Margins getInsets() { + return mRulesEngine.getEditor().getCanvasControl().getInsets(fqcn); + } }; } @@ -451,6 +456,16 @@ class ClientRulesEngine implements IClientRulesEngine { return (int) (px * 160 / dpi); } + public int dpToPx(int dp) { + ConfigurationComposite config = mRulesEngine.getEditor().getConfigurationComposite(); + float dpi = config.getDensity().getDpiValue(); + return (int) (dp * dpi / 160); + } + + public int screenToLayout(int pixels) { + return (int) (pixels / mRulesEngine.getEditor().getCanvasControl().getScale()); + } + String createNewFragmentClass(IJavaProject javaProject) { NewClassWizardPage page = new NewClassWizardPage(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java index e5f3e15..deb566e 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java @@ -575,6 +575,19 @@ public class RulesEngine { return mInsertType; } + // ---- Deletion ---- + + public void callOnRemovingChildren(AndroidXmlEditor editor, NodeProxy parentNode, + List<INode> children) { + if (parentNode != null) { + UiViewElementNode parentUiNode = parentNode.getNode(); + IViewRule parentRule = loadRule(parentUiNode); + if (parentRule != null) { + parentRule.onRemovingChildren(children, parentNode); + } + } + } + // ---- private --- /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java index abe4a83..9234d73 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java @@ -18,17 +18,22 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gre; import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; +import static com.android.ide.common.layout.LayoutConstants.FQCN_BUTTON; +import static com.android.ide.common.layout.LayoutConstants.FQCN_SPINNER; +import static com.android.ide.common.layout.LayoutConstants.FQCN_TOGGLE_BUTTON; import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX; import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; import com.android.annotations.VisibleForTesting; -import com.android.ide.common.api.ResizePolicy; import com.android.ide.common.api.IViewMetadata.FillPreference; +import com.android.ide.common.api.Margins; +import com.android.ide.common.api.ResizePolicy; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.IconFactory; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.resources.Density; import com.android.util.Pair; import org.w3c.dom.Document; @@ -697,4 +702,96 @@ public class ViewMetadataRepository { } } } + + /** + * Are insets supported yet? This flag indicates whether the {@link #getInsets} method + * can return valid data, such that clients can avoid doing any work computing the + * current theme or density if there's no chance that valid insets will be returned + */ + public static final boolean INSETS_SUPPORTED = false; + + /** + * Returns the insets of widgets with the given fully qualified name, in the given + * theme and the given screen density. + * + * @param fqcn the fully qualified name of the view + * @param density the screen density + * @param theme the theme name + * @return the insets of the visual bounds relative to the view info bounds, or null + * if not known or if there are no insets + */ + public static Margins getInsets(String fqcn, Density density, String theme) { + if (INSETS_SUPPORTED) { + // Some sample data measured manually for common themes and widgets. + if (fqcn.equals(FQCN_BUTTON)) { + if (density == Density.HIGH) { + if (theme.startsWith(HOLO_PREFIX)) { + // Theme.Holo, Theme.Holo.Light, WVGA + return new Margins(5, 5, 5, 5); + } else { + // Theme.Light, WVGA + return new Margins(4, 4, 0, 7); + } + } else if (density == Density.MEDIUM) { + if (theme.startsWith(HOLO_PREFIX)) { + // Theme.Holo, Theme.Holo.Light, WVGA + return new Margins(3, 3, 3, 3); + } else { + // Theme.Light, HVGA + return new Margins(2, 2, 0, 4); + } + } else if (density == Density.LOW) { + if (theme.startsWith(HOLO_PREFIX)) { + // Theme.Holo, Theme.Holo.Light, QVGA + return new Margins(2, 2, 2, 2); + } else { + // Theme.Light, QVGA + return new Margins(1, 3, 0, 4); + } + } + } else if (fqcn.equals(FQCN_TOGGLE_BUTTON)) { + if (density == Density.HIGH) { + if (theme.startsWith(HOLO_PREFIX)) { + // Theme.Holo, Theme.Holo.Light, WVGA + return new Margins(5, 5, 5, 5); + } else { + // Theme.Light, WVGA + return new Margins(2, 2, 0, 5); + } + } else if (density == Density.MEDIUM) { + if (theme.startsWith(HOLO_PREFIX)) { + // Theme.Holo, Theme.Holo.Light, WVGA + return new Margins(3, 3, 3, 3); + } else { + // Theme.Light, HVGA + return new Margins(0, 1, 0, 3); + } + } else if (density == Density.LOW) { + if (theme.startsWith(HOLO_PREFIX)) { + // Theme.Holo, Theme.Holo.Light, QVGA + return new Margins(2, 2, 2, 2); + } else { + // Theme.Light, QVGA + return new Margins(2, 2, 0, 4); + } + } + } else if (fqcn.equals(FQCN_SPINNER)) { + if (density == Density.HIGH) { + if (!theme.startsWith(HOLO_PREFIX)) { + // Theme.Light, WVGA + return new Margins(3, 4, 2, 8); + } // Doesn't render on Holo! + } else if (density == Density.MEDIUM) { + if (!theme.startsWith(HOLO_PREFIX)) { + // Theme.Light, HVGA + return new Margins(1, 1, 0, 4); + } + } + } + } + + return null; + } + + private static final String HOLO_PREFIX = "Theme.Holo"; //$NON-NLS-1$ } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml index 3ce531d..511d775 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml @@ -38,13 +38,13 @@ relatedTo="EditText,AutoCompleteTextView,MultiAutoCompleteTextView"> <view name="Large Text" - init="android:textAppearance=?android:attr/textAppearanceLarge" /> + init="android:textAppearance=?android:attr/textAppearanceLarge,android:text=Large Text" /> <view name="Medium Text" - init="android:textAppearance=?android:attr/textAppearanceMedium" /> + init="android:textAppearance=?android:attr/textAppearanceMedium,android:text=Medium Text" /> <view name="Small Text" - init="android:textAppearance=?android:attr/textAppearanceSmall" /> + init="android:textAppearance=?android:attr/textAppearanceSmall,android:text=Small Text" /> </view> <view class="android.widget.Button" @@ -156,6 +156,10 @@ <category name="Layouts"> <view + class="android.widget.GridLayout" + fill="opposite" + render="skip" /> + <view class="android.widget.LinearLayout" name="LinearLayout (Vertical)" init="android:orientation=vertical" @@ -191,6 +195,10 @@ fill="opposite" resize="vertical" render="skip" /> + <view + class="android.widget.Space" + fill="opposite" + render="skip" /> </category> <category name="Composite"> @@ -328,6 +336,9 @@ class="android.gesture.GestureOverlayView" render="skip" /> <view + class="android.view.TextureView" + render="skip" /> + <view class="android.view.SurfaceView" render="skip" /> <view diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java index 38a5e6b..6a50689 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java @@ -316,7 +316,7 @@ public final class ManifestEditor extends AndroidXmlEditor { for (UiElementNode ui_node : nodeList) { if (ui_node.getDescriptor().getXmlName().equals(nodeType)) { - for (UiAttributeNode attr : ui_node.getUiAttributes()) { + for (UiAttributeNode attr : ui_node.getAllUiAttributes()) { if (attr.getDescriptor().getXmlLocalName().equals( AndroidManifestDescriptors.ANDROID_NAME_ATTR)) { if (attr.getCurrentValue().equals(className)) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/UiElementPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/UiElementPart.java index 5e7ca30..c2e4f0f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/UiElementPart.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/UiElementPart.java @@ -244,7 +244,7 @@ public class UiElementPart extends ManifestSectionPart { @Override public boolean isDirty() { if (mUiElementNode != null && !super.isDirty()) { - for (UiAttributeNode ui_attr : mUiElementNode.getUiAttributes()) { + for (UiAttributeNode ui_attr : mUiElementNode.getAllUiAttributes()) { if (ui_attr.isDirty()) { markDirty(); break; @@ -269,7 +269,7 @@ public class UiElementPart extends ManifestSectionPart { if (mUiElementNode != null) { mEditor.wrapEditXmlModel(new Runnable() { public void run() { - for (UiAttributeNode ui_attr : mUiElementNode.getUiAttributes()) { + for (UiAttributeNode ui_attr : mUiElementNode.getAllUiAttributes()) { ui_attr.commit(); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java index 6506b58..3f7fadd 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java @@ -49,6 +49,7 @@ import org.eclipse.core.runtime.IStatus; import org.eclipse.jface.viewers.StyledString; import org.eclipse.ui.views.properties.IPropertyDescriptor; import org.eclipse.ui.views.properties.IPropertySource; +import org.eclipse.wst.xml.core.internal.document.ElementImpl; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -84,6 +85,7 @@ import java.util.Set; * The class implements {@link IPropertySource}, in order to fill the Eclipse property tab when * an element is selected. The {@link AttributeDescriptor} are used property descriptors. */ +@SuppressWarnings("restriction") // XML model public class UiElementNode implements IPropertySource { /** List of prefixes removed from android:id strings when creating short descriptions. */ @@ -119,7 +121,7 @@ public class UiElementNode implements IPropertySource { /** A read-only view of the UI children node collection. */ private List<UiElementNode> mReadOnlyUiChildren; /** A read-only view of the UI attributes collection. */ - private Collection<UiAttributeNode> mReadOnlyUiAttributes; + private Collection<UiAttributeNode> mCachedAllUiAttributes; /** A map of hidden attribute descriptors. Key is the XML name. */ private Map<String, AttributeDescriptor> mCachedHiddenAttributes; /** An optional list of {@link IUiUpdateListener}. Most element nodes will not have any @@ -181,7 +183,7 @@ public class UiElementNode implements IPropertySource { */ private void clearAttributes() { mUiAttributes = null; - mReadOnlyUiAttributes = null; + mCachedAllUiAttributes = null; mCachedHiddenAttributes = null; mUnknownUiAttributes = new HashSet<UiAttributeNode>(); } @@ -598,17 +600,27 @@ public class UiElementNode implements IPropertySource { } /** + * Returns a collection containing all the known attributes as well as + * all the unknown ui attributes. + * * @return A read-only version of the attributes collection. */ - public Collection<UiAttributeNode> getUiAttributes() { - if (mReadOnlyUiAttributes == null) { - mReadOnlyUiAttributes = Collections.unmodifiableCollection( - getInternalUiAttributes().values()); + public Collection<UiAttributeNode> getAllUiAttributes() { + if (mCachedAllUiAttributes == null) { + + List<UiAttributeNode> allValues = + new ArrayList<UiAttributeNode>(getInternalUiAttributes().values()); + allValues.addAll(mUnknownUiAttributes); + + mCachedAllUiAttributes = Collections.unmodifiableCollection(allValues); } - return mReadOnlyUiAttributes; + return mCachedAllUiAttributes; } /** + * Returns all the unknown ui attributes, that is those we found defined in the + * actual XML but that we don't have descriptors for. + * * @return A read-only version of the unknown attributes collection. */ public Collection<UiAttributeNode> getUnknownUiAttributes() { @@ -637,8 +649,7 @@ public class UiElementNode implements IPropertySource { } // get the error value from the attributes. - Collection<UiAttributeNode> attributes = getInternalUiAttributes().values(); - for (UiAttributeNode attribute : attributes) { + for (UiAttributeNode attribute : getAllUiAttributes()) { if (attribute.hasError()) { return true; } @@ -880,11 +891,7 @@ public class UiElementNode implements IPropertySource { * This is called by the UI when the embedding part needs to be committed. */ public void commit() { - for (UiAttributeNode uiAttr : getInternalUiAttributes().values()) { - uiAttr.commit(); - } - - for (UiAttributeNode uiAttr : mUnknownUiAttributes) { + for (UiAttributeNode uiAttr : getAllUiAttributes()) { uiAttr.commit(); } } @@ -896,13 +903,7 @@ public class UiElementNode implements IPropertySource { * loaded from the model. */ public boolean isDirty() { - for (UiAttributeNode uiAttr : getInternalUiAttributes().values()) { - if (uiAttr.isDirty()) { - return true; - } - } - - for (UiAttributeNode uiAttr : mUnknownUiAttributes) { + for (UiAttributeNode uiAttr : getAllUiAttributes()) { if (uiAttr.isDirty()) { return true; } @@ -949,6 +950,15 @@ public class UiElementNode implements IPropertySource { mXmlNode = doc.createElement(elementName); + // If this element does not have children, mark it as an empty tag + // such that the XML looks like <tag/> instead of <tag></tag> + if (!mDescriptor.hasChildren()) { + if (mXmlNode instanceof ElementImpl) { + ElementImpl element = (ElementImpl) mXmlNode; + element.setEmptyTag(true); + } + } + Node xmlNextSibling = null; UiElementNode uiNextSibling = getUiNextSibling(); @@ -1368,7 +1378,7 @@ public class UiElementNode implements IPropertySource { } // Clone the current list of unknown attributes. We'll then remove from this list when - // we still attributes which are still unknown. What will be left are the old unknown + // we find attributes which are still unknown. What will be left are the old unknown // attributes that have been deleted in the current XML attribute list. @SuppressWarnings("unchecked") HashSet<UiAttributeNode> deleted = (HashSet<UiAttributeNode>) mUnknownUiAttributes.clone(); @@ -1418,6 +1428,7 @@ public class UiElementNode implements IPropertySource { // Remove from the internal list unknown attributes that have been deleted from the xml for (UiAttributeNode a : deleted) { mUnknownUiAttributes.remove(a); + mCachedAllUiAttributes = null; } } } @@ -1435,6 +1446,7 @@ public class UiElementNode implements IPropertySource { UiAttributeNode uiAttr = desc.createUiNode(this); uiAttr.setDirty(true); mUnknownUiAttributes.add(uiAttr); + mCachedAllUiAttributes = null; return uiAttr; } @@ -1539,10 +1551,7 @@ public class UiElementNode implements IPropertySource { */ public boolean commitDirtyAttributesToXml() { boolean result = false; - HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); - - for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) { - UiAttributeNode uiAttr = entry.getValue(); + for (UiAttributeNode uiAttr : getAllUiAttributes()) { if (uiAttr.isDirty()) { result |= commitAttributeToXml(uiAttr, uiAttr.getCurrentValue()); uiAttr.setDirty(false); @@ -1668,21 +1677,21 @@ public class UiElementNode implements IPropertySource { // Try with all internal attributes UiAttributeNode uiAttr = setInternalAttrValue( - getInternalUiAttributes().values(), attrXmlName, attrNsUri, value, override); + getAllUiAttributes(), attrXmlName, attrNsUri, value, override); if (uiAttr != null) { return uiAttr; } - // Look at existing unknown (a.k.a. custom) attributes - uiAttr = setInternalAttrValue( - getUnknownUiAttributes(), attrXmlName, attrNsUri, value, override); - if (uiAttr == null) { // Failed to find the attribute. For non-android attributes that is mostly expected, - // in which case we just create a new custom one. - - uiAttr = addUnknownAttribute(attrXmlName, attrXmlName, attrNsUri); - // FIXME: The will create the attribute, but not actually set the value on it... + // in which case we just create a new custom one. As a side effect, we'll find the + // attribute descriptor via getAllUiAttributes(). + addUnknownAttribute(attrXmlName, attrXmlName, attrNsUri); + + // We've created the attribute, but not actually set the value on it, so let's do it. + // Try with the updated internal attributes. + uiAttr = setInternalAttrValue( + getAllUiAttributes(), attrXmlName, attrNsUri, value, override); } return uiAttr; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java index dce8160..f2b9a55 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java @@ -755,7 +755,7 @@ public class ExtractStringRefactoring extends Refactoring { name = name.substring(pos + 1); } - for (UiAttributeNode attrNode : currentUiNode.getUiAttributes()) { + for (UiAttributeNode attrNode : currentUiNode.getAllUiAttributes()) { if (attrNode.getDescriptor().getXmlLocalName().equals(name)) { AttributeDescriptor desc = attrNode.getDescriptor(); if (desc instanceof ReferenceAttributeDescriptor) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/MarginChooser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/MarginChooser.java index 1d4fc13..3c32d96 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/MarginChooser.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/MarginChooser.java @@ -153,7 +153,7 @@ public class MarginChooser extends SelectionStatusDialog implements Listener { new Label(container, SWT.NONE); mErrorLabel = new Label(container, SWT.WRAP); mErrorLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, 2, 1)); - + mErrorLabel.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_RED)); return container; } @@ -185,7 +185,7 @@ public class MarginChooser extends SelectionStatusDialog implements Listener { // Users are allowed to enter non-numbers here, not an error } if (isNumber) { - String message = String.format("Hint: Use \"%1$sdip\" instead", input); + String message = String.format("Hint: Use \"%1$sdp\" instead", input); mErrorLabel.setText(message); } else { mErrorLabel.setText(""); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreationPage.java index 6b6ade0..3b1b59e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreationPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreationPage.java @@ -31,11 +31,12 @@ import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; import com.android.ide.eclipse.adt.internal.wizards.newproject.NewTestProjectCreationPage.TestInfo; +import com.android.sdklib.AndroidVersion; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.SdkConstants; import com.android.sdklib.internal.project.ProjectProperties; -import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; import com.android.sdklib.internal.project.ProjectProperties.PropertyType; +import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; import com.android.sdklib.xml.AndroidManifest; import com.android.sdklib.xml.ManifestData; import com.android.sdklib.xml.ManifestData.Activity; @@ -1032,7 +1033,8 @@ public class NewProjectCreationPage extends WizardPage { // We do if one of two conditions are met: if (target != null) { boolean setMinSdk = false; - int apiLevel = target.getVersion().getApiLevel(); + AndroidVersion version = target.getVersion(); + int apiLevel = version.getApiLevel(); // 1. Has the user not manually edited the SDK field yet? If so, keep // updating it to the selected value. if (!mMinSdkModifiedByUser) { @@ -1053,9 +1055,15 @@ public class NewProjectCreationPage extends WizardPage { } } if (setMinSdk) { + String minSdk; + if (version.isPreview()) { + minSdk = version.getCodename(); + } else { + minSdk = Integer.toString(apiLevel); + } try { mInternalMinSdkUpdate = true; - mMinSdkVersionField.setText(Integer.toString(apiLevel)); + mMinSdkVersionField.setText(minSdk); } finally { mInternalMinSdkUpdate = false; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java index 87ccf09..8d9e0c6 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java @@ -18,6 +18,7 @@ package com.android.ide.eclipse.adt.internal.wizards.newxmlfile; import static com.android.ide.common.layout.LayoutConstants.HORIZONTAL_SCROLL_VIEW; +import static com.android.ide.common.layout.LayoutConstants.LINEAR_LAYOUT; import static com.android.ide.common.layout.LayoutConstants.SCROLL_VIEW; import static com.android.ide.common.layout.LayoutConstants.VALUE_FILL_PARENT; import static com.android.ide.common.layout.LayoutConstants.VALUE_MATCH_PARENT; @@ -181,8 +182,13 @@ class NewXmlFileCreationPage extends WizardPage { return mRootSeed; } - /** Returns the default root element that should be selected by default. Can be null. */ - String getDefaultRoot() { + /** + * Returns the default root element that should be selected by default. Can be + * null. + * + * @param project the associated project, or null if not known + */ + String getDefaultRoot(IProject project) { return mDefaultRoot; } @@ -210,8 +216,9 @@ class NewXmlFileCreationPage extends WizardPage { * root element of the generated XML file. When null, no extra attributes are inserted. * * @param project the project to get the attributes for + * @param root the selected root element string, never null */ - String getDefaultAttrs(IProject project) { + String getDefaultAttrs(IProject project, String root) { return mDefaultAttrs; } @@ -246,16 +253,33 @@ class NewXmlFileCreationPage extends WizardPage { "An XML file that describes a screen layout.", // tooltip ResourceFolderType.LAYOUT, // folder type AndroidTargetData.DESCRIPTOR_LAYOUT, // root seed - "LinearLayout", // default root + LINEAR_LAYOUT, // default root SdkConstants.NS_RESOURCES, // xmlns "", // not used, see below 1 // target API level ) { + + @Override + String getDefaultRoot(IProject project) { + // TODO: Use GridLayout by default for new SDKs + // (when we've ironed out all the usability issues) + //Sdk currentSdk = Sdk.getCurrent(); + //if (project != null && currentSdk != null) { + // IAndroidTarget target = currentSdk.getTarget(project); + // // fill_parent was renamed match_parent in API level 8 + // if (target != null && target.getVersion().getApiLevel() >= 13) { + // return GRID_LAYOUT; + // } + //} + + return LINEAR_LAYOUT; + }; + // The default attributes must be determined dynamically since whether // we use match_parent or fill_parent depends on the API level of the // project @Override - String getDefaultAttrs(IProject project) { + String getDefaultAttrs(IProject project, String root) { Sdk currentSdk = Sdk.getCurrent(); String fill = VALUE_FILL_PARENT; if (currentSdk != null) { @@ -266,11 +290,18 @@ class NewXmlFileCreationPage extends WizardPage { } } - return String.format( - "android:orientation=\"vertical\"\n" //$NON-NLS-1$ - + "android:layout_width=\"%1$s\"\n" //$NON-NLS-1$ + // Only set "vertical" orientation of LinearLayouts by default; + // for GridLayouts for example we want to rely on the real default + // of the layout + String size = String.format( + "android:layout_width=\"%1$s\"\n" //$NON-NLS-1$ + "android:layout_height=\"%2$s\"", //$NON-NLS-1$ fill, fill); + if (LINEAR_LAYOUT.equals(root)) { + return "android:orientation=\"vertical\"\n" + size; //$NON-NLS-1$ + } else { + return size; + } } @Override @@ -278,7 +309,7 @@ class NewXmlFileCreationPage extends WizardPage { // Create vertical linear layouts inside new scroll views if (SCROLL_VIEW.equals(root) || HORIZONTAL_SCROLL_VIEW.equals(root)) { return " <LinearLayout " //$NON-NLS-1$ - + getDefaultAttrs(project).replace('\n', ' ') + + getDefaultAttrs(project, root).replace('\n', ' ') + "></LinearLayout>\n"; //$NON-NLS-1$ } return null; @@ -1226,7 +1257,7 @@ class NewXmlFileCreationPage extends WizardPage { } int index = 0; // default is to select the first one - String defaultRoot = type.getDefaultRoot(); + String defaultRoot = type.getDefaultRoot(getProject()); if (defaultRoot != null) { index = roots.indexOf(defaultRoot); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java index 7e7362d..87afa11 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java @@ -144,7 +144,7 @@ public class NewXmlFileWizard extends Wizard implements INewWizard { return null; } - String attrs = type.getDefaultAttrs(mMainPage.getProject()); + String attrs = type.getDefaultAttrs(mMainPage.getProject(), root); String child = type.getChild(mMainPage.getProject(), root); return createXmlFile(file, xmlns, root, attrs, child); @@ -223,7 +223,7 @@ public class NewXmlFileWizard extends Wizard implements INewWizard { */ public static boolean canCreateXmlFile(ResourceFolderType folderType) { TypeInfo typeInfo = NewXmlFileCreationPage.getTypeInfo(folderType); - return typeInfo != null && (typeInfo.getDefaultRoot() != null || + return typeInfo != null && (typeInfo.getDefaultRoot(null /*project*/) != null || typeInfo.getRootSeed() instanceof String); } @@ -239,11 +239,11 @@ public class NewXmlFileWizard extends Wizard implements INewWizard { ResourceFolderType folderType) { TypeInfo type = NewXmlFileCreationPage.getTypeInfo(folderType); String xmlns = type.getXmlns(); - String root = type.getDefaultRoot(); + String root = type.getDefaultRoot(project); if (root == null) { root = type.getRootSeed().toString(); } - String attrs = type.getDefaultAttrs(project); + String attrs = type.getDefaultAttrs(project, root); return createXmlFile(file, xmlns, root, attrs, null); } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/GridLayoutRuleTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/GridLayoutRuleTest.java new file mode 100644 index 0000000..f048365 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/GridLayoutRuleTest.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.common.layout; + + +public class GridLayoutRuleTest extends LayoutTestBase { + @Override + public void testDummy() { + } +} 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 b29424e..3fee553 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 @@ -281,6 +281,16 @@ public class LayoutTestBase extends TestCase { fail("Not supported in tests yet"); return null; } + + public int screenToLayout(int pixels) { + fail("Not supported in tests yet"); + return 0; + } + + public int dpToPx(int dp) { + fail("Not supported in tests yet"); + return 0; + } } public void testDummy() { diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LinearLayoutRuleTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LinearLayoutRuleTest.java index eb2158e..ab1d1c6 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LinearLayoutRuleTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LinearLayoutRuleTest.java @@ -30,11 +30,12 @@ import com.android.ide.common.api.IMenuCallback; import com.android.ide.common.api.INode; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.MenuAction; +import com.android.ide.common.api.MenuAction.Choices; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; -import com.android.ide.common.api.MenuAction.Choices; import java.util.List; +import java.util.Locale; import java.util.Map; /** Test the {@link LinearLayoutRule} */ @@ -380,6 +381,24 @@ public class LinearLayoutRuleTest extends LayoutTestBase { assertEquals("-1", LinearLayoutRule.formatFloatAttribute(-1f)); } + public void testFormatFloatValueLocale() throws Exception { + // Ensure that the layout float values aren't affected by + // locale settings, like using commas instead of of periods + Locale originalDefaultLocale = Locale.getDefault(); + + try { + Locale.setDefault(Locale.FRENCH); + + // Ensure that this is a locale which uses a comma instead of a period: + assertEquals("5,24", String.format("%.2f", 5.236f)); + + // Ensure that the formatFloatAttribute is immune + assertEquals("1.50", LinearLayoutRule.formatFloatAttribute(1.5f)); + } finally { + Locale.setDefault(originalDefaultLocale); + } + } + // Left to test: // Check inserting at last pos with multiple children // Check inserting with no bounds rectangle for dragged element diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestGraphics.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestGraphics.java index 5088bac..3bc9e53 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestGraphics.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestGraphics.java @@ -146,6 +146,10 @@ public class TestGraphics implements IGraphics { mDrawn.add("drawArrow(" + x1 + "," + y1 + "," + x2 + "," + y2 + ")"); } + public void drawPoint(int x, int y) { + mDrawn.add("drawPoint(" + x + "," + y + ")"); + } + private static String rectToString(Rect rect) { return "Rect[" + rect.x + "," + rect.y + "," + rect.w + "," + rect.h + "]"; } 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 new file mode 100644 index 0000000..44eb443 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/grid/GridModelTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.common.layout.grid; + +import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; +import static com.android.ide.common.layout.LayoutConstants.ATTR_COLUMN_COUNT; +import static com.android.ide.common.layout.LayoutConstants.FQCN_BUTTON; + +import com.android.ide.common.api.Rect; +import com.android.ide.common.layout.LayoutTestBase; +import com.android.ide.common.layout.TestNode; + + +public class GridModelTest extends LayoutTestBase { + public void testRemoveFlag() { + assertEquals("left", GridModel.removeFlag("top", "top|left")); + assertEquals("left", GridModel.removeFlag("top", "top | left")); + assertEquals("top", GridModel.removeFlag("left", "top|left")); + assertEquals("top", GridModel.removeFlag("left", "top | left")); + assertEquals("left | center", GridModel.removeFlag("top", "top | left | center")); + assertEquals(null, GridModel.removeFlag("top", "top")); + } + + public void testReadModel1() { + 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); + assertEquals(3, model.declaredColumnCount); + assertEquals(1, model.actualColumnCount); + assertEquals(1, model.actualRowCount); + + targetNode.add(TestNode.create(FQCN_BUTTON).id("@+id/Button1")); + targetNode.add(TestNode.create(FQCN_BUTTON).id("@+id/Button2")); + targetNode.add(TestNode.create(FQCN_BUTTON).id("@+id/Button3")); + targetNode.add(TestNode.create(FQCN_BUTTON).id("@+id/Button4")); + + model = new GridModel(null, targetNode); + assertEquals(3, model.declaredColumnCount); + assertEquals(3, model.actualColumnCount); + assertEquals(2, model.actualRowCount); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiElementNodeTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiElementNodeTest.java index ccf4e83..a5149ae 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiElementNodeTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiElementNodeTest.java @@ -79,7 +79,7 @@ public class UiElementNodeTest extends TestCase { assertSame(mManifestDesc, ui.getDescriptor()); assertNull(ui.getUiParent()); assertEquals(0, ui.getUiChildren().size()); - assertEquals(0, ui.getUiAttributes().size()); + assertEquals(0, ui.getAllUiAttributes().size()); } /** @@ -139,14 +139,14 @@ public class UiElementNodeTest extends TestCase { ui.loadFromXmlNode(root); assertEquals("manifest", ui.getDescriptor().getXmlName()); assertEquals(1, ui.getUiChildren().size()); - assertEquals(0, ui.getUiAttributes().size()); + assertEquals(0, ui.getAllUiAttributes().size()); // get /manifest/application Iterator<UiElementNode> ui_child_it = ui.getUiChildren().iterator(); UiElementNode application = ui_child_it.next(); assertEquals("application", application.getDescriptor().getXmlName()); assertEquals(0, application.getUiChildren().size()); - assertEquals(0, application.getUiAttributes().size()); + assertEquals(0, application.getAllUiAttributes().size()); } @@ -161,21 +161,21 @@ public class UiElementNodeTest extends TestCase { ui.loadFromXmlNode(root); assertEquals("manifest", ui.getDescriptor().getXmlName()); assertEquals(2, ui.getUiChildren().size()); - assertEquals(0, ui.getUiAttributes().size()); + assertEquals(0, ui.getAllUiAttributes().size()); // get /manifest/application Iterator<UiElementNode> ui_child_it = ui.getUiChildren().iterator(); UiElementNode application = ui_child_it.next(); assertEquals("application", application.getDescriptor().getXmlName()); assertEquals(0, application.getUiChildren().size()); - assertEquals(0, application.getUiAttributes().size()); + assertEquals(0, application.getAllUiAttributes().size()); assertEquals(0, application.getUiSiblingIndex()); // get /manifest/permission UiElementNode first_permission = ui_child_it.next(); assertEquals("permission", first_permission.getDescriptor().getXmlName()); assertEquals(0, first_permission.getUiChildren().size()); - assertEquals(0, first_permission.getUiAttributes().size()); + assertEquals(0, first_permission.getAllUiAttributes().size()); assertEquals(1, first_permission.getUiSiblingIndex()); } @@ -206,58 +206,58 @@ public class UiElementNodeTest extends TestCase { ui.loadFromXmlNode(root); assertEquals("manifest", ui.getDescriptor().getXmlName()); assertEquals(3, ui.getUiChildren().size()); - assertEquals(0, ui.getUiAttributes().size()); + assertEquals(0, ui.getAllUiAttributes().size()); // get /manifest/application Iterator<UiElementNode> ui_child_it = ui.getUiChildren().iterator(); UiElementNode application = ui_child_it.next(); assertEquals("application", application.getDescriptor().getXmlName()); assertEquals(4, application.getUiChildren().size()); - assertEquals(0, application.getUiAttributes().size()); + assertEquals(0, application.getAllUiAttributes().size()); // get /manifest/application/activity #1 Iterator<UiElementNode> app_child_it = application.getUiChildren().iterator(); UiElementNode first_activity = app_child_it.next(); assertEquals("activity", first_activity.getDescriptor().getXmlName()); assertEquals(0, first_activity.getUiChildren().size()); - assertEquals(0, first_activity.getUiAttributes().size()); + assertEquals(0, first_activity.getAllUiAttributes().size()); // get /manifest/application/activity #2 UiElementNode second_activity = app_child_it.next(); assertEquals("activity", second_activity.getDescriptor().getXmlName()); assertEquals(1, second_activity.getUiChildren().size()); - assertEquals(0, second_activity.getUiAttributes().size()); + assertEquals(0, second_activity.getAllUiAttributes().size()); // get /manifest/application/activity #2/intent-filter #1 Iterator<UiElementNode> activity_child_it = second_activity.getUiChildren().iterator(); UiElementNode intent_filter = activity_child_it.next(); assertEquals("intent-filter", intent_filter.getDescriptor().getXmlName()); assertEquals(0, intent_filter.getUiChildren().size()); - assertEquals(0, intent_filter.getUiAttributes().size()); + assertEquals(0, intent_filter.getAllUiAttributes().size()); // get /manifest/application/provider #1 UiElementNode first_provider = app_child_it.next(); assertEquals("provider", first_provider.getDescriptor().getXmlName()); assertEquals(0, first_provider.getUiChildren().size()); - assertEquals(0, first_provider.getUiAttributes().size()); + assertEquals(0, first_provider.getAllUiAttributes().size()); // get /manifest/application/provider #2 UiElementNode second_provider = app_child_it.next(); assertEquals("provider", second_provider.getDescriptor().getXmlName()); assertEquals(0, second_provider.getUiChildren().size()); - assertEquals(0, second_provider.getUiAttributes().size()); + assertEquals(0, second_provider.getAllUiAttributes().size()); // get /manifest/permission #1 UiElementNode first_permission = ui_child_it.next(); assertEquals("permission", first_permission.getDescriptor().getXmlName()); assertEquals(0, first_permission.getUiChildren().size()); - assertEquals(0, first_permission.getUiAttributes().size()); + assertEquals(0, first_permission.getAllUiAttributes().size()); // get /manifest/permission #1 UiElementNode second_permission = ui_child_it.next(); assertEquals("permission", second_permission.getDescriptor().getXmlName()); assertEquals(0, second_permission.getUiChildren().size()); - assertEquals(0, second_permission.getUiAttributes().size()); + assertEquals(0, second_permission.getAllUiAttributes().size()); } diff --git a/eclipse/plugins/com.android.ide.eclipse.traceview/src/com/android/ide/eclipse/traceview/editors/TraceviewEditor.java b/eclipse/plugins/com.android.ide.eclipse.traceview/src/com/android/ide/eclipse/traceview/editors/TraceviewEditor.java index b713e5e..3ac5bcc 100644 --- a/eclipse/plugins/com.android.ide.eclipse.traceview/src/com/android/ide/eclipse/traceview/editors/TraceviewEditor.java +++ b/eclipse/plugins/com.android.ide.eclipse.traceview/src/com/android/ide/eclipse/traceview/editors/TraceviewEditor.java @@ -15,15 +15,16 @@ */ package com.android.ide.eclipse.traceview.editors; +import com.android.ide.eclipse.traceview.TraceviewPlugin; import com.android.traceview.ColorController; import com.android.traceview.DmTraceReader; import com.android.traceview.MethodData; import com.android.traceview.ProfileView; -import com.android.traceview.ProfileView.MethodHandler; import com.android.traceview.SelectionController; import com.android.traceview.TimeLineView; import com.android.traceview.TraceReader; import com.android.traceview.TraceUnits; +import com.android.traceview.ProfileView.MethodHandler; import org.eclipse.core.filesystem.EFS; import org.eclipse.core.filesystem.IFileStore; @@ -35,7 +36,9 @@ import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Status; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.search.IJavaSearchConstants; import org.eclipse.jdt.core.search.SearchEngine; @@ -56,6 +59,7 @@ import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorSite; @@ -66,6 +70,7 @@ import org.eclipse.ui.part.EditorPart; import org.eclipse.ui.part.FileEditorInput; import java.io.File; +import java.io.IOException; import java.net.URI; public class TraceviewEditor extends EditorPart implements MethodHandler { @@ -257,37 +262,46 @@ public class TraceviewEditor extends EditorPart implements MethodHandler { @Override public void createPartControl(Composite parent) { mParent = parent; - TraceReader reader = new DmTraceReader(mFilename, false); - reader.getTraceUnits().setTimeScale(TraceUnits.TimeScale.MilliSeconds); - - mContents = new Composite(mParent, SWT.NONE); - - Display display = mContents.getDisplay(); - ColorController.assignMethodColors(display, reader.getMethods()); - SelectionController selectionController = new SelectionController(); - - GridLayout gridLayout = new GridLayout(1, false); - gridLayout.marginWidth = 0; - gridLayout.marginHeight = 0; - gridLayout.horizontalSpacing = 0; - gridLayout.verticalSpacing = 0; - mContents.setLayout(gridLayout); - - Color darkGray = display.getSystemColor(SWT.COLOR_DARK_GRAY); - - // Create a sash form to separate the timeline view (on top) - // and the profile view (on bottom) - SashForm sashForm1 = new SashForm(mContents, SWT.VERTICAL); - sashForm1.setBackground(darkGray); - sashForm1.SASH_WIDTH = 3; - GridData data = new GridData(GridData.FILL_BOTH); - sashForm1.setLayoutData(data); - - // Create the timeline view - new TimeLineView(sashForm1, reader, selectionController); - - // Create the profile view - new ProfileView(sashForm1, reader, selectionController).setMethodHandler(this); + try { + TraceReader reader = new DmTraceReader(mFilename, false); + reader.getTraceUnits().setTimeScale(TraceUnits.TimeScale.MilliSeconds); + + mContents = new Composite(mParent, SWT.NONE); + + Display display = mContents.getDisplay(); + ColorController.assignMethodColors(display, reader.getMethods()); + SelectionController selectionController = new SelectionController(); + + GridLayout gridLayout = new GridLayout(1, false); + gridLayout.marginWidth = 0; + gridLayout.marginHeight = 0; + gridLayout.horizontalSpacing = 0; + gridLayout.verticalSpacing = 0; + mContents.setLayout(gridLayout); + + Color darkGray = display.getSystemColor(SWT.COLOR_DARK_GRAY); + + // Create a sash form to separate the timeline view (on top) + // and the profile view (on bottom) + SashForm sashForm1 = new SashForm(mContents, SWT.VERTICAL); + sashForm1.setBackground(darkGray); + sashForm1.SASH_WIDTH = 3; + GridData data = new GridData(GridData.FILL_BOTH); + sashForm1.setLayoutData(data); + + // Create the timeline view + new TimeLineView(sashForm1, reader, selectionController); + + // Create the profile view + new ProfileView(sashForm1, reader, selectionController).setMethodHandler(this); + } catch (IOException e) { + Label l = new Label(parent, 0); + l.setText("Failed to read the stack trace."); + + Status status = new Status(IStatus.ERROR, TraceviewPlugin.PLUGIN_ID, + "Failed to read the stack trace.", e); + TraceviewPlugin.getDefault().getLog().log(status); + } mParent.layout(); } diff --git a/files/ant/main_rules.xml b/files/ant/main_rules.xml index 996a70d..97e9efb 100644 --- a/files/ant/main_rules.xml +++ b/files/ant/main_rules.xml @@ -202,7 +202,8 @@ <echo>Converting compiled files and external libraries into ${intermediate.dex.file}...</echo> <apply executable="${dx}" failonerror="true" parallel="true"> <arg value="--dex" /> - <arg value="--output=${intermediate.dex.file}" /> + <arg value="--output" /> + <arg path="${intermediate.dex.file}" /> <extra-parameters /> <arg line="${verbose.option}" /> <arg path="${out.dex.input.absolute.dir}" /> diff --git a/sdkmanager/app/src/com/android/sdkmanager/Main.java b/sdkmanager/app/src/com/android/sdkmanager/Main.java index ee5879c..f7cd55a 100644 --- a/sdkmanager/app/src/com/android/sdkmanager/Main.java +++ b/sdkmanager/app/src/com/android/sdkmanager/Main.java @@ -903,6 +903,28 @@ public class Main { } /** + * Displays the ABIs valid for the given target. + */ + private void displayAbiList(IAndroidTarget target, String message) { + String[] abis = target.getAbiList(); + mSdkLog.printf(message); + if (abis != null) { + boolean first = true; + for (String skin : abis) { + if (first == false) { + mSdkLog.printf(", "); + } else { + first = false; + } + mSdkLog.printf(skin); + } + mSdkLog.printf("\n"); + } else { + mSdkLog.printf("no ABIs.\n"); + } + } + + /** * Displays the list of available AVDs for the given AvdManager. * * @param avdManager @@ -1105,14 +1127,26 @@ public class Main { oldAvdInfo = avdManager.getAvd(avdName, false /*validAvdOnly*/); } - // NOTE: need to update with command line processor selectivity + String abiType = mSdkCommandLine.getParamAbi(); + if (target != null && (abiType == null || abiType.length() == 0)) { + String[] abis = target.getAbiList(); + if (abis != null && abis.length == 1) { + // Auto-select the single ABI available + abiType = abis[0]; + mSdkLog.printf("Auto-selecting single ABI %1$s", abiType); + } else { + displayAbiList(target, "Valid ABIs: "); + errorAndExit("This platform has more than one ABI. Please specify one using --%1$s.", + SdkCommandLine.KEY_ABI); + + } + } - String preferredAbi = SdkConstants.ABI_ARMEABI; @SuppressWarnings("unused") // newAvdInfo is never read, yet useful for debugging AvdInfo newAvdInfo = avdManager.createAvd(avdFolder, avdName, target, - preferredAbi, + abiType, skin, mSdkCommandLine.getParamSdCard(), hardwareConfig, diff --git a/sdkmanager/app/src/com/android/sdkmanager/SdkCommandLine.java b/sdkmanager/app/src/com/android/sdkmanager/SdkCommandLine.java index 6dabb84..3bfc12b 100644 --- a/sdkmanager/app/src/com/android/sdkmanager/SdkCommandLine.java +++ b/sdkmanager/app/src/com/android/sdkmanager/SdkCommandLine.java @@ -83,6 +83,7 @@ class SdkCommandLine extends CommandLineProcessor { public static final String KEY_SNAPSHOT = "snapshot"; //$NON-NLS-1$ public static final String KEY_COMPACT = "compact"; //$NON-NLS-1$ public static final String KEY_EOL_NULL = "null"; //$NON-NLS-1$ + public static final String KEY_ABI = "abi"; //$NON-NLS-1$ /** * Action definitions for SdkManager command line. @@ -205,6 +206,10 @@ class SdkCommandLine extends CommandLineProcessor { define(Mode.BOOLEAN, false, VERB_CREATE, OBJECT_AVD, "a", KEY_SNAPSHOT, //$NON-NLS-1$ "Place a snapshots file in the AVD, to enable persistence.", false); + define(Mode.STRING, false, + VERB_CREATE, OBJECT_AVD, "b", KEY_ABI, //$NON-NLS-1$ + "The ABI to use for the AVD. The default is to auto-select the ABI if the platform has only one ABI for its system images.", + null); // --- delete avd --- @@ -537,6 +542,11 @@ class SdkCommandLine extends CommandLineProcessor { return ((String) getValue(null, null, KEY_FILTER)); } + /** Helper to retrieve the --abi value. */ + public String getParamAbi() { + return ((String) getValue(null, null, KEY_ABI)); + } + /** Helper to retrieve the --proxy-host value. */ public String getParamProxyHost() { return ((String) getValue(null, null, KEY_PROXY_HOST)); diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/AvdManager.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/AvdManager.java index 7a3b23c..1ca7c07 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/AvdManager.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/AvdManager.java @@ -527,8 +527,7 @@ public class AvdManager { File userdataSrc = new File(imagePath, USERDATA_IMG); if (userdataSrc.exists() == false && target.isPlatform() == false) { - imagePath = - target.getParent().getImagePath(abiType); + imagePath = target.getParent().getImagePath(abiType); userdataSrc = new File(imagePath, USERDATA_IMG); } @@ -836,9 +835,8 @@ public class AvdManager { return null; } - /** Copy the nominated file to the given destination. - * @param source - * @param destination + /** + * Copy the nominated file to the given destination. * * @throws FileNotFoundException * @throws IOException diff --git a/traceview/src/com/android/traceview/DmTraceReader.java b/traceview/src/com/android/traceview/DmTraceReader.java index fbcd13e..ac44a09 100644 --- a/traceview/src/com/android/traceview/DmTraceReader.java +++ b/traceview/src/com/android/traceview/DmTraceReader.java @@ -70,7 +70,7 @@ public class DmTraceReader extends TraceReader { // A regex for matching the thread "id name" lines in the .key file private static final Pattern mIdNamePattern = Pattern.compile("(\\d+)\t(.*)"); //$NON-NLS-1$ - public DmTraceReader(String traceFileName, boolean regression) { + public DmTraceReader(String traceFileName, boolean regression) throws IOException { mTraceFileName = traceFileName; mRegression = regression; mPropertiesMap = new HashMap<String, String>(); @@ -87,15 +87,10 @@ public class DmTraceReader extends TraceReader { generateTrees(); } - void generateTrees() { - try { - long offset = parseKeys(); - parseData(offset); - analyzeData(); - } catch (IOException e) { - System.err.println(e.getMessage()); - System.exit(1); - } + void generateTrees() throws IOException { + long offset = parseKeys(); + parseData(offset); + analyzeData(); } @Override @@ -105,25 +100,17 @@ public class DmTraceReader extends TraceReader { return mProfileProvider; } - private MappedByteBuffer mapFile(String filename, long offset) { + private MappedByteBuffer mapFile(String filename, long offset) throws IOException { MappedByteBuffer buffer = null; - try { - FileInputStream dataFile = new FileInputStream(filename); - File file = new File(filename); - FileChannel fc = dataFile.getChannel(); - buffer = fc.map(FileChannel.MapMode.READ_ONLY, offset, file.length() - offset); - buffer.order(ByteOrder.LITTLE_ENDIAN); - } catch (FileNotFoundException ex) { - System.err.println(ex.getMessage()); - System.exit(1); - } catch (IOException ex) { - System.err.println(ex.getMessage()); - System.exit(1); - } - + FileInputStream dataFile = new FileInputStream(filename); + File file = new File(filename); + FileChannel fc = dataFile.getChannel(); + buffer = fc.map(FileChannel.MapMode.READ_ONLY, offset, file.length() - offset); + buffer.order(ByteOrder.LITTLE_ENDIAN); + return buffer; } - + private void readDataFileHeader(MappedByteBuffer buffer) { int magic = buffer.getInt(); if (magic != TRACE_MAGIC) { @@ -170,7 +157,7 @@ public class DmTraceReader extends TraceReader { } } - private void parseData(long offset) { + private void parseData(long offset) throws IOException { MappedByteBuffer buffer = mapFile(mTraceFileName, offset); readDataFileHeader(buffer); @@ -430,7 +417,7 @@ public class DmTraceReader extends TraceReader { if (line == null) { throw new IOException("Key section does not have an *end marker"); } - + // Calculate how much we have read from the file so far. The // extra byte is for the line ending not included by readLine(). offset += line.length() + 1; @@ -602,7 +589,7 @@ public class DmTraceReader extends TraceReader { for (Call call : mCallList) { call.updateName(); } - + if (mRegression) { dumpMethodStats(); } @@ -664,7 +651,7 @@ public class DmTraceReader extends TraceReader { call.getMethodData().getName()); } } - + private void dumpMethodStats() { System.out.print("\nMethod Stats\n"); System.out.print("Excl Cpu Incl Cpu Excl Real Incl Real Calls Method\n"); diff --git a/traceview/src/com/android/traceview/MainWindow.java b/traceview/src/com/android/traceview/MainWindow.java index cf949a6..0b07163 100644 --- a/traceview/src/com/android/traceview/MainWindow.java +++ b/traceview/src/com/android/traceview/MainWindow.java @@ -253,8 +253,16 @@ public class MainWindow extends ApplicationWindow { } } - reader = new DmTraceReader(traceName, regression); + try { + reader = new DmTraceReader(traceName, regression); + } catch (IOException e) { + System.err.printf("Failed to read the trace file"); + e.printStackTrace(); + System.exit(1); + return; + } } + reader.getTraceUnits().setTimeScale(TraceUnits.TimeScale.MilliSeconds); Display.setAppName("Traceview"); diff --git a/traceview/src/com/android/traceview/PropertiesDialog.java b/traceview/src/com/android/traceview/PropertiesDialog.java index cbae0a8..9f5eff9 100644 --- a/traceview/src/com/android/traceview/PropertiesDialog.java +++ b/traceview/src/com/android/traceview/PropertiesDialog.java @@ -68,6 +68,7 @@ public class PropertiesDialog extends Dialog { TableViewerColumn propertyColumn = new TableViewerColumn(tableViewer, SWT.NONE); propertyColumn.getColumn().setText("Property"); propertyColumn.setLabelProvider(new ColumnLabelProvider() { + @Override @SuppressWarnings("unchecked") public String getText(Object element) { Entry<String, String> entry = (Entry<String, String>) element; @@ -79,6 +80,7 @@ public class PropertiesDialog extends Dialog { TableViewerColumn valueColumn = new TableViewerColumn(tableViewer, SWT.NONE); valueColumn.getColumn().setText("Value"); valueColumn.setLabelProvider(new ColumnLabelProvider() { + @Override @SuppressWarnings("unchecked") public String getText(Object element) { Entry<String, String> entry = (Entry<String, String>) element; diff --git a/traceview/src/com/android/traceview/TimeBase.java b/traceview/src/com/android/traceview/TimeBase.java index b6b23cb..b29a46b 100644 --- a/traceview/src/com/android/traceview/TimeBase.java +++ b/traceview/src/com/android/traceview/TimeBase.java @@ -26,44 +26,36 @@ interface TimeBase { public long getElapsedInclusiveTime(ProfileData profileData); public static final class CpuTimeBase implements TimeBase { - @Override public long getTime(ThreadData threadData) { return threadData.getCpuTime(); } - @Override public long getElapsedInclusiveTime(MethodData methodData) { return methodData.getElapsedInclusiveCpuTime(); } - @Override public long getElapsedExclusiveTime(MethodData methodData) { return methodData.getElapsedExclusiveCpuTime(); } - @Override public long getElapsedInclusiveTime(ProfileData profileData) { return profileData.getElapsedInclusiveCpuTime(); } } public static final class RealTimeBase implements TimeBase { - @Override public long getTime(ThreadData threadData) { return threadData.getRealTime(); } - @Override public long getElapsedInclusiveTime(MethodData methodData) { return methodData.getElapsedInclusiveRealTime(); } - @Override public long getElapsedExclusiveTime(MethodData methodData) { return methodData.getElapsedExclusiveRealTime(); } - @Override public long getElapsedInclusiveTime(ProfileData profileData) { return profileData.getElapsedInclusiveRealTime(); } |