aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java1
-rw-r--r--eclipse/dictionary.txt8
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/GridLayout.pngbin0 -> 466 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/Space.pngbin0 -> 468 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/TextureView.pngbin0 -> 560 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DrawingStyle.java10
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DropFeedback.java6
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IClientRulesEngine.java19
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IGraphics.java11
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewMetadata.java7
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewRule.java19
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Margins.java4
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java6
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java262
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java22
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java430
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java37
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java20
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java48
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ResizeState.java6
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/addcol.pngbin0 -> 479 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java713
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridLayoutPainter.java301
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridMatch.java150
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridModel.java1959
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DependencyGraph.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/GuidelinePainter.java6
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/removecol.pngbin0 -> 469 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/showgrid.pngbin0 -> 477 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/snap.pngbin0 -> 575 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java45
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java18
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ProjectCallback.java8
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java12
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java45
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java29
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java10
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java9
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java11
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java14
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java25
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureToolTip.java157
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java29
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java20
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java41
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java11
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java7
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandles.java15
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java16
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java18
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java17
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java14
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java15
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java13
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java99
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml17
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/UiElementPart.java4
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java79
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/MarginChooser.java4
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreationPage.java14
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java51
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java8
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/GridLayoutRuleTest.java23
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java10
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LinearLayoutRuleTest.java21
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/TestGraphics.java4
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/grid/GridModelTest.java56
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiElementNodeTest.java30
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.traceview/src/com/android/ide/eclipse/traceview/editors/TraceviewEditor.java78
-rw-r--r--files/ant/main_rules.xml3
-rw-r--r--sdkmanager/app/src/com/android/sdkmanager/Main.java40
-rw-r--r--sdkmanager/app/src/com/android/sdkmanager/SdkCommandLine.java10
-rw-r--r--sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/AvdManager.java8
-rw-r--r--traceview/src/com/android/traceview/DmTraceReader.java47
-rw-r--r--traceview/src/com/android/traceview/MainWindow.java10
-rw-r--r--traceview/src/com/android/traceview/PropertiesDialog.java2
-rw-r--r--traceview/src/com/android/traceview/TimeBase.java8
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
new file mode 100644
index 0000000..1aa4165
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/GridLayout.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/Space.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/Space.png
new file mode 100644
index 0000000..58afbe4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/Space.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/TextureView.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/TextureView.png
new file mode 100644
index 0000000..353b6b7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/TextureView.png
Binary files differ
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
new file mode 100644
index 0000000..21391ef
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/addcol.png
Binary files differ
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
new file mode 100644
index 0000000..c41261a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/removecol.png
Binary files differ
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
new file mode 100644
index 0000000..6f7bf91
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/showgrid.png
Binary files differ
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
new file mode 100644
index 0000000..b50a16e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/snap.png
Binary files differ
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();
}