diff options
author | Tor Norbye <tnorbye@google.com> | 2010-11-15 15:35:15 -0800 |
---|---|---|
committer | Tor Norbye <tnorbye@google.com> | 2010-11-19 12:43:16 -0800 |
commit | a2d7874ed23bfc2fa7665cc84901e0f4781b4e51 (patch) | |
tree | 58f81c6ae19363a31335b3874c0fa617d5ea3396 /eclipse/plugins/com.android.ide.eclipse.adt/src/com | |
parent | a6ed5f2625d825a85cb7579b7017b1ea02a984ee (diff) | |
download | sdk-a2d7874ed23bfc2fa7665cc84901e0f4781b4e51.zip sdk-a2d7874ed23bfc2fa7665cc84901e0f4781b4e51.tar.gz sdk-a2d7874ed23bfc2fa7665cc84901e0f4781b4e51.tar.bz2 |
Add per-view custom initialization logic
This changeset adds support for adding custom-logic to initialize
views, both to add children and default attributes and to customize
layout attributes when added to a new parent.
First, there is a new "onCreate" hook which is called to notify a view
rule that an instance of its corresponding view has been created. This
lets the ViewRule perform custom initialization of the object. The
ViewRule is told what type of insertion occurred, such that it can
distinguish between a newly created view, a view that is the result of
a copy/paste, and a view that is part of a move operation.
The changeset adds a number of new ViewRules which take advantage of
this:
- A TabHost rule creates the various skeleton children that are
required, such as a TabWidget child with id @android:id/tabs and a
FrameLayout child with id @android:id/tabcontent
- A DialerFilter rule creates the mandatory EditText children ("hint"
and "primary")
- The HorizontalScrollView rule creates a horizontal LinearLayout child
- The ImageButton and ImageViewButtons initialize the "src" attribute
to a sample image
- The MapViewRule initializes the apiKey attribute
In addition, views are also notified when a new view is added as a
child, such that they can perform additional customizations, in the
form of an "onInsert" event.
The most important application of this is LinearLayoutRule, which uses
this to set reasonable defaults for the layout_width and layout_height
parameters. It uses metadata (which is currently built into ADT but
would ideally migrate into our XML config files) to determine whether
a given child prefers to grow horizontally, grow vertically, both, or
neither, depending on the surrounding parent context. For example, an
EditText will default to filling the parent width if it is in a
vertical LinearLayout, but it will not grow vertically in a horizontal
linear layout. And so on. Various other rules also use the onInsert
event to tweak children attributes. A ScrollView will for example
always initialize its single child to match parent.
Views can now also add plain menu items into the context menu, and the
TableViewRule adds one such action: "Add Row", which appends a new row
into the table.
The Palette Preview code also invokes these creation hooks, such that
if you for example drag a DialerFilter it can properly render since
the mandatory children are created up front. This required various
changes to the preview code to be able to handle XML edits by the
rules.
Finally, this changeset includes various other misc changes that I
performed at the same time:
- Removed SWT dependency from the ViewRule classes (SWT Rectangle use
in Rect)
- Fixed AbsoluteLayout unit test (issue 3203560)
- Fixed positioning of the preview outline in LinearLayout when only
one of the dimensions are clipped due to a smaller target layout
Change-Id: I5956fe4e7a31a20b8dd2f9d9b0c1f90e2f75d68a
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com')
34 files changed, 1347 insertions, 180 deletions
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 647c6a0..a17af01 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 @@ -28,8 +28,6 @@ import com.android.sdklib.annotations.Nullable; * <b>NOTE: This is not a public or final API; if you rely on this be prepared * to adjust your code for the next tools release.</b> * </p> - * - * @see IViewRule#RULES_ENGINE */ public interface IClientRulesEngine { @@ -57,6 +55,14 @@ public interface IClientRulesEngine { IViewRule loadRule(String fqcn); /** + * Returns the metadata associated with the given fully qualified class name. + * + * @param fqcn a fully qualified class name for an Android view class + * @return the metadata associated with the given fully qualified class name. + */ + IViewMetadata getMetadata(String fqcn); + + /** * Displays the given message string in an alert dialog with an "OK" button. */ void displayAlert(String message); 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 new file mode 100644 index 0000000..9397ef8 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewMetadata.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2010 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.api; + +/** + * Metadata about a particular view. The metadata for a View can be found by asking the + * {@link IClientRulesEngine} for the metadata for a given class via + * {@link IClientRulesEngine#getMetadata}. + */ +public interface IViewMetadata { + /** + * Returns true if this view is a potential parent (e.g. it <b>can</b> have children). + * + * @return true if this view can have children + */ + public boolean isParent(); + + /** + * Returns the display name views of this type (a name suitable to display to the + * user, normally capitalized and usually but not necessarily tied to the + * implementation class). To be clear, a specific view may have an id attribute and a + * text attribute, <b>neither</b> of these is the display name. Instead, the class + * android.widget.ZoomControls may have the display name "Zoom Controls", and an + * individual view created into a layout can for example have the id "ZoomControl01". + * + * @return the user visible name of views of this type (never null) + */ + public String getDisplayName(); + + /** + * Returns the tooltip for this view, if any + * + * @return a tooltip, or null + */ + public String getTooltip(); + + /** + * Returns the {@link FillPreference} of this view + * + * @return the {@link FillPreference} of this view + */ + public FillPreference getFillPreference(); + + /** + * Types of fill behavior that views can prefer. + * <p> + * TODO: Consider better names. FillPolicy? Stretchiness? + */ + public enum FillPreference { + /** This view does not want to fill */ + NONE, + /** This view wants to always fill both horizontal and vertical */ + BOTH, + /** This view wants to fill horizontally but not vertically */ + WIDTH, + /** This view wants to fill vertically but not horizontally */ + HEIGHT, + /** + * This view wants to fill in the opposite dimension of the context, e.g. in a + * vertical context it wants to fill horizontally, and vice versa + */ + OPPOSITE, + /** This view wants to fill horizontally, but only in a vertical context */ + WIDTH_IN_VERTICAL, + /** This view wants to fill vertically, but only in a horizontal context */ + HEIGHT_IN_HORIZONTAL; + + /** + * Returns true if this view wants to fill horizontally, if the context is + * vertical or horizontal as indicated by the parameter. + * + * @param verticalContext If true, the context is vertical, otherwise it is + * horizontal. + * @return true if this view wants to fill horizontally + */ + public boolean fillHorizontally(boolean verticalContext) { + return (this == BOTH || this == WIDTH || + (verticalContext && (this == OPPOSITE || this == WIDTH_IN_VERTICAL))); + } + + /** + * Returns true if this view wants to fill vertically, if the context is + * vertical or horizontal as indicated by the parameter. + * + * @param verticalContext If true, the context is vertical, otherwise it is + * horizontal. + * @return true if this view wants to fill vertically + */ + public boolean fillVertically(boolean verticalContext) { + return (this == BOTH || this == HEIGHT || + (!verticalContext && (this == OPPOSITE || this == HEIGHT_IN_HORIZONTAL))); + } + } + +} 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 13699b4..0ec8eb3 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 @@ -17,7 +17,6 @@ package com.android.ide.common.api; import java.util.List; -import java.util.Map; /** @@ -45,13 +44,6 @@ import java.util.Map; public interface IViewRule { /** - * The name of the property that returns a {@link IClientRulesEngine} created for this - * rules. The instance lets rules use some methods from the rules engine, for example - * for accessing other rules. - */ - final static String RULES_ENGINE = "_rules_engine"; - - /** * This method is called by the rule engine when the script is first loaded. * It gives the rule a chance to initialize itself. * @@ -63,7 +55,7 @@ public interface IViewRule { * the engine during initialization and then use it later to invoke some of the * {@link IClientRulesEngine} methods for example to request user input. * @return True if this rule can handle the given FQCN. False if the rule can't handle the - * given FQCN, in which case the rule engine will find another rule matching a parent clas. + * given FQCN, in which case the rule engine will find another rule matching a parent class. */ boolean onInitialize(String fqcn, IClientRulesEngine engine); @@ -140,23 +132,6 @@ public interface IViewRule { INode childNode); - // ==== XML Creation ==== - - /** - * Returns the default attributes that a new XML element of this type should have - * when added to an XML layout file. Note that these defaults can be overridden by the - * specific code performing the insertion. - * - * TODO: - * - added=>created - * - list tuple(uri, local name, str: value) - * - gen id - * - * @return A map of attribute:values for a new element of this type. Can be null or empty. - */ - Map<?, ?> getDefaultAttributes(); - - // ==== Drag'n'drop support ==== /** @@ -215,4 +190,40 @@ public interface IViewRule { * @param pastedElements The elements being pasted. */ void onPaste(INode targetNode, IDragElement[] pastedElements); + + // ==== XML Creation ==== + + /** + * Called when a view for this rule is being created. This allows for the rule to + * customize the newly created object. Note that this method is called not just when a + * view is created from a palette drag, but when views are constructed via a drag-move + * (where views are created in the destination and then deleted from the source), and + * even when views are constructed programmatically from other view rules. The + * {@link InsertType} parameter can be used to distinguish the context for the + * insertion. For example, the <code>DialerFilterRule</code> will insert EditText children + * when a DialerFilter is first created, but not during a copy/paste or a move. + * + * @param node the newly created node (which will always be a View that applies to + * this {@link IViewRule}) + * @param parent the parent of the node (which may not yet contain the newly created + * node in its child list) + * @param insertType whether this node was created as part of a newly created view, or + * as a copy, or as a move, etc. + */ + void onCreate(INode node, INode parent, InsertType insertType); + + /** + * Called when a child for this view has been created and is being inserted into the + * view parent for which this {@link IViewRule} applies. Allows the parent to perform + * customizations of the object. As with {@link #onCreate}, the {@link InsertType} + * parameter can be used to handle new creation versus moves versus copy/paste + * operations differently. + * + * @param child the newly created node + * @param parent the parent of the newly created node (which may not yet contain the + * newly created node in its child list) + * @param insertType whether this node was created as part of a newly created view, or + * as a copy, or as a move, etc. + */ + void onChildInserted(INode child, INode parent, InsertType insertType); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/InsertType.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/InsertType.java new file mode 100644 index 0000000..15d3a98 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/InsertType.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2010 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.api; + +/** + * An enumerated type of different insertion events, such as an insertion from a + * copy/paste operation or as the first half of a move operation. + */ +public enum InsertType { + /** The view is newly created (by for example a palette drag) */ + CREATE, + + /** The view is being inserted here because it was moved from somewhere else */ + MOVE, + + /** + * The view is being inserted here as a result of a copy/paste from elsewhere + * (including drags, but not from the palette) + */ + PASTE; +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/MenuAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/MenuAction.java index 210e565..d82049d 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/MenuAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/MenuAction.java @@ -152,7 +152,7 @@ public abstract class MenuAction { /** * The base class for {@link Toggle} and {@link Choices}. */ - public static abstract class Action extends MenuAction { + public static class Action extends MenuAction { /** * A callback executed when the action is selected in the context menu. @@ -176,7 +176,7 @@ public abstract class MenuAction { * @param callback The callback executed when the action is selected. * Must not be null. */ - private Action(String id, String title, String groupId, IMenuCallback callback) { + public Action(String id, String title, String groupId, IMenuCallback callback) { super(id, title); mGroupId = groupId; mCallback = callback; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Rect.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Rect.java index 32caf62..b34d729 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Rect.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/Rect.java @@ -16,7 +16,6 @@ package com.android.ide.common.api; -import org.eclipse.swt.graphics.Rectangle; /** @@ -49,11 +48,6 @@ public class Rect { } /** Initialize rectangle to the given values. They can be invalid. */ - public Rect(Rectangle swtRect) { - set(swtRect); - } - - /** Initialize rectangle to the given values. They can be invalid. */ public Rect set(int x, int y, int w, int h) { this.x = x; this.y = y; @@ -68,12 +62,6 @@ public class Rect { return this; } - /** Initialize rectangle to match the given one. */ - public Rect set(Rectangle swtRect) { - set(swtRect.x, swtRect.y, swtRect.width, swtRect.height); - return this; - } - /** Returns a new instance of a rectangle with the same values. */ public Rect copy() { return new Rect(x, y, w, h); @@ -161,11 +149,6 @@ public class Rect { } return this.x == rhs.x && this.y == rhs.y && this.w == rhs.w && this.h == rhs.h; - - } else if (obj instanceof Rectangle) { - Rectangle rhs = (Rectangle) obj; - return this.x == rhs.x && this.y == rhs.y && - this.w == rhs.width && this.h == rhs.height; } return false; @@ -173,10 +156,10 @@ public class Rect { @Override public int hashCode() { - int h = x; - h ^= ((y >> 8) & 0x0FFFFFF) | ((y & 0x00000FF) << 24); - h ^= ((w >> 16) & 0x000FFFF) | ((w & 0x000FFFF) << 16); - h ^= ((h >> 24) & 0x00000FF) | ((h & 0x0FFFFFF) << 8); - return h; + int hc = x; + hc ^= ((y >> 8) & 0x0FFFFFF) | ((y & 0x00000FF) << 24); + hc ^= ((w >> 16) & 0x000FFFF) | ((w & 0x000FFFF) << 16); + hc ^= ((h >> 24) & 0x00000FF) | ((h & 0x0FFFFFF) << 8); + return hc; } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsListViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsListViewRule.java new file mode 100644 index 0000000..cd1b0fc --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsListViewRule.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2010 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 com.android.ide.common.api.IViewRule; + +/** + * An {@link IViewRule} for android.widget.AbsListViewRule + */ +public class AbsListViewRule extends IgnoredLayoutRule { + + // GridViews and ListViews are not configurable via XML + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseView.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseView.java index 15871d7..045f36d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseView.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseView.java @@ -26,6 +26,7 @@ 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.InsertType; import com.android.ide.common.api.MenuAction; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; @@ -45,7 +46,7 @@ import java.util.Set; * Common IViewRule processing to all view and layout classes. */ public class BaseView implements IViewRule { - private IClientRulesEngine mRulesEngine; + protected IClientRulesEngine mRulesEngine; /** * Namespace for the Android resource XML, i.e. @@ -53,23 +54,42 @@ public class BaseView implements IViewRule { */ public static String ANDROID_URI = "http://schemas.android.com/apk/res/android"; //$NON-NLS-1$ + /** The fully qualified class name of an EditText view */ + public static final String FQCN_EDIT_TEXT = "android.widget.EditText"; //$NON-NLS-1$ + + /** The fully qualified class name of a LinearLayout view */ + public static final String FQCN_LINEAR_LAYOUT = "android.widget.LinearLayout"; //$NON-NLS-1$ + + /** The fully qualified class name of a FrameLayout view */ + public static final String FQCN_FRAME_LAYOUT = "android.widget.FrameLayout"; //$NON-NLS-1$ + + /** The fully qualified class name of a TableRow view */ + public static final String FQCN_TABLE_ROW = "android.widget.TableRow"; //$NON-NLS-1$ + + /** The fully qualified class name of a TabWidget view */ + public static final String FQCN_TAB_WIDGET = "android.widget.TabWidget"; //$NON-NLS-1$ + // Some common Android layout attribute names used by the view rules. // All these belong to the attribute namespace ANDROID_URI. - public static String ATTR_ID = "id"; //$NON-NLS-1$ + public static final String ATTR_ID = "id"; //$NON-NLS-1$ - public static String ATTR_TEXT = "text"; //$NON-NLS-1$ + public static final String ATTR_TEXT = "text"; //$NON-NLS-1$ - public static String ATTR_LAYOUT_WIDTH = "layout_width"; //$NON-NLS-1$ + public static final String ATTR_LAYOUT_WIDTH = "layout_width"; //$NON-NLS-1$ - public static String ATTR_LAYOUT_HEIGHT = "layout_height"; //$NON-NLS-1$ + public static final String ATTR_LAYOUT_HEIGHT = "layout_height"; //$NON-NLS-1$ + + public static final String ATTR_SRC = "src"; //$NON-NLS-1$ + + public static final String ATTR_LAYOUT_BELOW = "layout_below"; //$NON-NLS-1$ // Some common Android layout attribute values used by the view rules. - public static String VALUE_FILL_PARENT = "fill_parent"; //$NON-NLS-1$ + public static final String VALUE_FILL_PARENT = "fill_parent"; //$NON-NLS-1$ // like fill_parent for API 8 - public static String VALUE_MATCH_PARENT = "match_parent"; //$NON-NLS-1$ + public static final String VALUE_MATCH_PARENT = "match_parent"; //$NON-NLS-1$ - public static String VALUE_WRAP_CONTENT = "wrap_content"; //$NON-NLS-1$ + public static final String VALUE_WRAP_CONTENT = "wrap_content"; //$NON-NLS-1$ // Cache of attributes. Key is FQCN of a node mixed with its view hierarchy // parent. Values are a custom map as needed by getContextMenu. @@ -98,11 +118,6 @@ public class BaseView implements IViewRule { return null; } - public Map<?, ?> getDefaultAttributes() { - // The base rule does not have any custom default attributes. - return null; - } - // === Context Menu === /** @@ -531,4 +546,26 @@ public class BaseView implements IViewRule { return mChoices; } } + + /** + * Returns a source attribute value which points to a sample image. This is typically + * used to provide an initial image shown on ImageButtons, etc. There is no guarantee + * that the source pointed to by this method actually exists. + * + * @return a source attribute to use for sample images, never null + */ + protected 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 + // in the @android namespace. + + return "@drawable/icon"; //$NON-NLS-1$ + } + + public void onCreate(INode node, INode parent, InsertType insertType) { + } + + public void onChildInserted(INode node, INode parent, InsertType insertType) { + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/DialerFilterRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/DialerFilterRule.java new file mode 100644 index 0000000..54b511e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/DialerFilterRule.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2010 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 com.android.ide.common.api.INode; +import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.InsertType; + +/** + * An {@link IViewRule} for android.widget.DialerFilterRule. + */ +public class DialerFilterRule extends BaseView { + + @Override + public void onCreate(INode node, INode parent, InsertType insertType) { + super.onCreate(node, parent, insertType); + + // A DialerFilter requires a couple of nested EditTexts with fixed ids: + if (insertType == InsertType.CREATE) { + INode hint = node.appendChild(FQCN_EDIT_TEXT); + hint.setAttribute(ANDROID_URI, BaseView.ATTR_TEXT, "Hint"); + hint.setAttribute(ANDROID_URI, ATTR_ID, "@android:id/hint"); //$NON-NLS-1$ + hint.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT); + + INode primary = node.appendChild(FQCN_EDIT_TEXT); + primary.setAttribute(ANDROID_URI, BaseView.ATTR_TEXT, "Primary"); + primary.setAttribute(ANDROID_URI, ATTR_ID, "@android:id/primary"); //$NON-NLS-1$ + primary.setAttribute(ANDROID_URI, ATTR_LAYOUT_BELOW, + "@android:id/hint"); //$NON-NLS-1$ + primary.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT); + + + // What do we initialize the icon to? + //INode icon = node.appendChild("android.widget.ImageView"); //$NON-NLS-1$ + //icon.setAttribute(ANDROID_URI, ATTR_ID, "@android:id/icon"); //$NON-NLS-1$ + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/HorizontalScrollViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/HorizontalScrollViewRule.java new file mode 100644 index 0000000..547dc04 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/HorizontalScrollViewRule.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2010 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 com.android.ide.common.api.INode; +import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.InsertType; + +/** + * An {@link IViewRule} for android.widget.HorizontalScrollView. + */ +public class HorizontalScrollViewRule extends BaseView { + + @Override + public void onChildInserted(INode child, INode parent, InsertType insertType) { + super.onChildInserted(child, parent, insertType); + + // The child of the ScrollView should fill in both directions + child.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT); + child.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, VALUE_FILL_PARENT); + } + + @Override + public void onCreate(INode node, INode parent, InsertType insertType) { + super.onCreate(node, parent, insertType); + + if (insertType == InsertType.CREATE) { + // Insert a horizontal linear layout which is commonly used with horizontal scrollbars + // as described by the documentation for HorizontalScrollbars. + INode linearLayout = node.appendChild(FQCN_LINEAR_LAYOUT); + linearLayout.setAttribute(ANDROID_URI, LinearLayoutRule.ATTR_ORIENTATION, + LinearLayoutRule.VALUE_HORIZONTAL); + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ImageButtonRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ImageButtonRule.java new file mode 100644 index 0000000..7a23f17 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ImageButtonRule.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2010 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 com.android.ide.common.api.INode; +import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.InsertType; + +/** + * An {@link IViewRule} for android.widget.ImageButtonRule. + */ +public class ImageButtonRule extends BaseView { + + @Override + public void onCreate(INode node, INode parent, InsertType insertType) { + super.onCreate(node, parent, insertType); + + if (insertType == InsertType.CREATE) { + node.setAttribute(ANDROID_URI, ATTR_SRC, getSampleImageSrc()); + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ImageViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ImageViewRule.java new file mode 100644 index 0000000..67cd422 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ImageViewRule.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2010 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 com.android.ide.common.api.INode; +import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.InsertType; + +/** + * An {@link IViewRule} for android.widget.ImageViewRule. + */ +public class ImageViewRule extends BaseView { + + @Override + public void onCreate(INode node, INode parent, InsertType insertType) { + super.onCreate(node, parent, insertType); + + if (insertType == InsertType.CREATE) { + node.setAttribute(ANDROID_URI, ATTR_SRC, getSampleImageSrc()); + } + } + +} 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 e5334f8..e31424c 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,10 +24,13 @@ import com.android.ide.common.api.IGraphics; import com.android.ide.common.api.IMenuCallback; import com.android.ide.common.api.INode; import com.android.ide.common.api.INodeHandler; +import com.android.ide.common.api.IViewMetadata; import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.InsertType; 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.IViewMetadata.FillPreference; import java.util.ArrayList; import java.util.List; @@ -47,34 +50,57 @@ public class LinearLayoutRule extends BaseLayout { */ @Override public List<MenuAction> getContextMenu(final INode selectedNode) { + if (supportsOrientation()) { + String curr_orient = selectedNode.getStringAttr(ANDROID_URI, ATTR_ORIENTATION); + if (curr_orient == null || curr_orient.length() == 0) { + curr_orient = VALUE_HORIZONTAL; + } - String curr_orient = selectedNode.getStringAttr(ANDROID_URI, ATTR_ORIENTATION); - if (curr_orient == null || curr_orient.length() == 0) { - curr_orient = VALUE_HORIZONTAL; + IMenuCallback onChange = new IMenuCallback() { + public void action(MenuAction action, final String valueId, Boolean newValue) { + String actionId = action.getId(); + final INode node = selectedNode; + + if (actionId.equals("_orientation")) { //$NON-NLS-1$ + node.editXml("Change LinearLayout " + ATTR_ORIENTATION, new INodeHandler() { + public void handle(INode n) { + node.setAttribute(ANDROID_URI, ATTR_ORIENTATION, valueId); + } + }); + } + } + }; + + return concatenate(super.getContextMenu(selectedNode), + new MenuAction.Choices("_orientation", "Orientation", //$NON-NLS-1$ + mapify( + "horizontal", "Horizontal", //$NON-NLS-1$ + "vertical", "Vertical" //$NON-NLS-1$ + ), + curr_orient, onChange)); + } else { + return super.getContextMenu(selectedNode); } + } - IMenuCallback onChange = new IMenuCallback() { - public void action(MenuAction action, final String valueId, Boolean newValue) { - String actionId = action.getId(); - final INode node = selectedNode; + /** + * Returns true if the given node represents a vertical linear layout. + * @param node the node to check layout orientation for + * @return true if the layout is in vertical mode, otherwise false + */ + protected boolean isVertical(INode node) { + // Horizontal is the default, so if no value is specified it is horizontal. + return VALUE_VERTICAL.equals(node.getStringAttr(ANDROID_URI, + ATTR_ORIENTATION)); + } - if (actionId.equals("_orientation")) { //$NON-NLS-1$ - node.editXml("Change LinearLayout " + ATTR_ORIENTATION, new INodeHandler() { - public void handle(INode n) { - node.setAttribute(ANDROID_URI, ATTR_ORIENTATION, valueId); - } - }); - } - } - }; - - return concatenate(super.getContextMenu(selectedNode), - new MenuAction.Choices("_orientation", "Orientation", //$NON-NLS-1$ - mapify( - "horizontal", "Horizontal", //$NON-NLS-1$ - "vertical", "Vertical" //$NON-NLS-1$ - ), - curr_orient, onChange)); + /** + * Returns true if this LinearLayout supports switching orientation. + * + * @return true if this layout supports orientations + */ + protected boolean supportsOrientation() { + return true; } // ==== Drag'n'drop support ==== @@ -91,8 +117,7 @@ public class LinearLayoutRule extends BaseLayout { return null; } - boolean isVertical = VALUE_VERTICAL.equals(targetNode.getStringAttr(ANDROID_URI, - ATTR_ORIENTATION)); + boolean isVertical = isVertical(targetNode); // Prepare a list of insertion points: X coords for horizontal, Y for // vertical. @@ -247,11 +272,28 @@ public class LinearLayoutRule extends BaseLayout { gc.useStyle(DrawingStyle.DROP_PREVIEW); for (IDragElement element : elements) { Rect bounds = element.getBounds(); - if (bounds.isValid() && (bounds.w > b.w || bounds.h > b.h)) { + if (bounds.isValid() && (bounds.w > b.w || bounds.h > b.h) && + node.getChildren().length == 0) { // The bounds of the child does not fully fit inside the target. - // Limit the bounds to the layout bounds. - Rect within = new Rect(b.x, b.y, - Math.min(bounds.w, b.w), Math.min(bounds.h, b.h)); + // Limit the bounds to the layout bounds (but only when there + // are no children, since otherwise positioning around the existing + // children gets difficult) + final int px, py, pw, ph; + if (bounds.w > b.w) { + px = b.x; + pw = b.w; + } else { + px = bounds.x + offsetX; + pw = bounds.w; + } + if (bounds.h > b.h) { + py = b.y; + ph = b.h; + } else { + py = bounds.y + offsetY; + ph = bounds.h; + } + Rect within = new Rect(px, py, pw, ph); gc.drawRect(within); } else { drawElement(gc, element, offsetX, offsetY); @@ -369,6 +411,25 @@ public class LinearLayoutRule extends BaseLayout { }); } + @Override + public void onChildInserted(INode node, INode parent, InsertType insertType) { + // Attempt to set fill-properties on newly added views such that for example, + // in a vertical layout, a text field defaults to filling horizontally, but not + // vertically. + String fqcn = node.getFqcn(); + IViewMetadata metadata = mRulesEngine.getMetadata(fqcn); + if (metadata != null) { + boolean vertical = isVertical(parent); + FillPreference fill = metadata.getFillPreference(); + if (fill.fillHorizontally(vertical)) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT); + } + if (fill.fillVertically(vertical)) { + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, VALUE_FILL_PARENT); + } + } + } + /** A possible match position */ private class MatchPos { /** The pixel distance */ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ListViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ListViewRule.java index 67a3cf3..347057a 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ListViewRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ListViewRule.java @@ -16,10 +16,9 @@ package com.android.ide.common.layout; +import com.android.ide.common.api.INode; import com.android.ide.common.api.IViewRule; - -import java.util.HashMap; -import java.util.Map; +import com.android.ide.common.api.InsertType; /** * An {@link IViewRule} for android.widget.ListView and all its derived classes. @@ -28,19 +27,10 @@ import java.util.Map; */ public class ListViewRule extends BaseView { - // ==== XML Creation ==== - - /** - * The default for new views is to be wrap_content on both width/height. - * However ListView is special in that ideally we want fill_parent width by - * default. - */ @Override - public Map<?, ?> getDefaultAttributes() { - // TODO: find a way to plug in the new value VALUE_MATCH_PARENT. - Map<String, String> result = new HashMap<String, String>(); - result.put(ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT); - return result; - } + public void onCreate(INode node, INode parent, InsertType insertType) { + super.onCreate(node, parent, insertType); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT); + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MapViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MapViewRule.java new file mode 100644 index 0000000..2eff369 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MapViewRule.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010 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 com.android.ide.common.api.INode; +import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.InsertType; + +/** + * An {@link IViewRule} for com.google.android.maps.MapView. + * <p> + * TODO: This class should be pulled out of the ADT and bundled with the add ons + * (not the core jar but an optional tool jar) + */ +public class MapViewRule extends BaseView { + + @Override + public void onCreate(INode node, INode parent, InsertType insertType) { + super.onCreate(node, parent, insertType); + + if (insertType == InsertType.CREATE) { + node.setAttribute(ANDROID_URI, "android:apiKey", //$NON-NLS-1$ + "Your API key: see " + //$NON-NLS-1$ + "http://code.google.com/android/add-ons/google-apis/mapkey.html"); //$NON-NLS-1$ + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ScrollViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ScrollViewRule.java new file mode 100644 index 0000000..5ce8bad --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ScrollViewRule.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2010 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 com.android.ide.common.api.INode; +import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.InsertType; + +/** + * An {@link IViewRule} for android.widget.ScrollView. + */ +public class ScrollViewRule extends BaseView { + + @Override + public void onChildInserted(INode child, INode parent, InsertType insertType) { + super.onChildInserted(child, parent, insertType); + + // The child of the ScrollView should fill in both directions + child.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT); + child.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, VALUE_FILL_PARENT); + } + + @Override + public void onCreate(INode node, INode parent, InsertType insertType) { + super.onCreate(node, parent, insertType); + + if (insertType == InsertType.CREATE) { + // Insert a default linear layout (which will in turn be registered as + // a child of this node and the create child method above will set its + // fill parent attributes, its id, etc. + node.appendChild(FQCN_LINEAR_LAYOUT); + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/SeekBarRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/SeekBarRule.java new file mode 100644 index 0000000..b6d184f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/SeekBarRule.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2010 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 com.android.ide.common.api.INode; +import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.InsertType; + +/** + * An {@link IViewRule} for android.widget.SeekBar + */ +public class SeekBarRule extends BaseView { + + @Override + public void onCreate(INode node, INode parent, InsertType insertType) { + super.onCreate(node, parent, insertType); + + // A SeekBar isn't useful with wrap_content because it packs itself down to + // almost no usable width -- so just make it grow in all layouts + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TabHostRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TabHostRule.java index 4885bb9..f47da68 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TabHostRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TabHostRule.java @@ -16,7 +16,9 @@ package com.android.ide.common.layout; +import com.android.ide.common.api.INode; import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.InsertType; /** * An {@link IViewRule} for android.widget.TabHost. @@ -26,6 +28,32 @@ public class TabHostRule extends IgnoredLayoutRule { // manipulate its children via the TabHost rather than directly manipulating // the child elements yourself, e.g. via addTab() etc. - // TODO: We should add a context menu here to add tabs. We should also add - // creation code to pre-populate a tab + @Override + public void onCreate(INode node, INode parent, InsertType insertType) { + super.onCreate(node, parent, insertType); + + if (insertType == InsertType.CREATE) { + // Configure default Table setup as described in the Table tutorial + node.setAttribute(ANDROID_URI, ATTR_ID, "@android:id/tabhost"); //$NON-NLS-1$ + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT); + node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, VALUE_FILL_PARENT); + + INode linear = node.appendChild(FQCN_LINEAR_LAYOUT); + linear.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT); + linear.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, VALUE_FILL_PARENT); + linear.setAttribute(ANDROID_URI, LinearLayoutRule.ATTR_ORIENTATION, + LinearLayoutRule.VALUE_VERTICAL); + + INode tab = linear.appendChild(FQCN_TAB_WIDGET); + tab.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT); + tab.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT); + tab.setAttribute(ANDROID_URI, ATTR_ID, "@android:id/tabs"); //$NON-NLS-1$ + + INode frame = linear.appendChild(FQCN_FRAME_LAYOUT); + frame.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT); + frame.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, VALUE_FILL_PARENT); + frame.setAttribute(ANDROID_URI, ATTR_ID, "@android:id/tabcontent"); //$NON-NLS-1$ + } + } + } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableLayoutRule.java new file mode 100644 index 0000000..a4d528c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableLayoutRule.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2010 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 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.InsertType; +import com.android.ide.common.api.MenuAction; + +import java.util.List; + +/** + * An {@link IViewRule} for android.widget.TableLayout. + */ +public class TableLayoutRule extends LinearLayoutRule { + // A table is a linear layout, but with a few differences: + // the default is vertical, not horizontal + // The fill of all children should be wrap_content + + @Override + protected boolean isVertical(INode node) { + // Tables are always vertical + return true; + } + + @Override + protected boolean supportsOrientation() { + return false; + } + + @Override + public void onChildInserted(INode node, INode parent, InsertType insertType) { + // Overridden to inhibit the setting of layout_width/layout_height since + // it should always be match_parent + } + + /** + * Add an explicit "Add Row" action to the context menu + */ + @Override + public List<MenuAction> getContextMenu(final INode selectedNode) { + IMenuCallback addTab = new IMenuCallback() { + public void action(MenuAction action, final String valueId, Boolean newValue) { + final INode node = selectedNode; + node.appendChild(FQCN_TABLE_ROW); + + } + }; + return concatenate(super.getContextMenu(selectedNode), + new MenuAction.Action("_addrow", "Add Row", //$NON-NLS-1$ + null, addTab)); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableRowRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableRowRule.java new file mode 100644 index 0000000..e734f4a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableRowRule.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2010 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 com.android.ide.common.api.INode; +import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.InsertType; + +/** + * An {@link IViewRule} for android.widget.TableRow. + */ +public class TableRowRule extends LinearLayoutRule { + @Override + protected boolean isVertical(INode node) { + return false; + } + + @Override + protected boolean supportsOrientation() { + return false; + } + + @Override + public void onChildInserted(INode child, INode parent, InsertType insertType) { + // Overridden to inhibit the setting of layout_width/layout_height since + // the table row will enforce match_parent and wrap_content for width and height + // respectively. + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java index 04abe34..9c31b11 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java @@ -127,10 +127,10 @@ public class AndroidConstants { /** Absolute path of the workspace root, i.e. "/" */ public final static String WS_ROOT = WS_SEP; - /** Absolute path of the resource folder, eg "/res".<br> This is a workspace path. */ + /** Absolute path of the resource folder, e.g. "/res".<br> This is a workspace path. */ public final static String WS_RESOURCES = WS_SEP + SdkConstants.FD_RESOURCES; - /** Absolute path of the resource folder, eg "/assets".<br> This is a workspace path. */ + /** Absolute path of the resource folder, e.g. "/assets".<br> This is a workspace path. */ public final static String WS_ASSETS = WS_SEP + SdkConstants.FD_ASSETS; /** Leaf of the javaDoc folder. Does not start with a separator. */ @@ -212,7 +212,7 @@ public class AndroidConstants { public final static String MARKER_ATTR_TYPE_PROVIDER = "provider"; //$NON-NLS-1$ /** - * Prefered compiler level, i.e. "1.5". + * Preferred compiler level, i.e. "1.5". */ public final static String COMPILER_COMPLIANCE_PREFERRED = "1.5"; //$NON-NLS-1$ /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java index a899bba..0c10363 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java @@ -198,7 +198,7 @@ public class IconFactory { gc.setAntialias(SWT.ON); gc.setTextAntialias(SWT.ON); - // image.setBackground() does not appear to have any affect; we must explicitly + // image.setBackground() does not appear to have any effect; we must explicitly // paint into the image the background color we want masked out later. // HOWEVER, alpha transparency does not work; we only get to mark a single color // as transparent. You might think we could pick a system color (to avoid having diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/IGraphicalLayoutEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/IGraphicalLayoutEditor.java index 3a22b64..c200e47 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/IGraphicalLayoutEditor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/IGraphicalLayoutEditor.java @@ -22,6 +22,7 @@ import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; import com.android.layoutlib.api.LayoutScene; import org.eclipse.core.resources.IFile; +import org.eclipse.draw2d.geometry.Rectangle; import org.eclipse.ui.IEditorPart; import java.util.Set; @@ -96,8 +97,10 @@ public interface IGraphicalLayoutEditor extends IEditorPart { * * @param model the model to be rendered, which can be different than the editor's own * {@link #getModel()}. - * @param width the width to use for the layout - * @param height the height to use for the layout + * @param width the width to use for the layout, or -1 to use the width of the screen + * associated with this editor + * @param height the height to use for the layout, or -1 to use the height of the screen + * associated with this editor * @param explodeNodes a set of nodes to explode, or null for none * @param transparentBackground If true, the rendering will <b>not</b> paint the * normal background requested by the theme, and it will instead paint the @@ -107,4 +110,10 @@ public interface IGraphicalLayoutEditor extends IEditorPart { abstract LayoutScene render(UiDocumentNode model, int width, int height, Set<UiElementNode> explodeNodes, boolean transparentBackground); + /** + * Returns the current bounds of the Android device screen, in canvas control pixels + * + * @return the bounds of the screen, never null + */ + abstract Rectangle getScreenBounds(); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java index ed8a205..41bf099 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java @@ -28,6 +28,7 @@ import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewEleme import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.OutlinePage2; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.PropertySheetPage2; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.Sdk; @@ -202,7 +203,7 @@ public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput, // Optional: set the default page. Eventually a default page might be // restored by selectDefaultPage() later based on the last page used by the user. // For example, to make the last page the default one (rather than the first page), - // un-comment this line: + // uncomment this line: // setActivePage(getPageCount() - 1); } @@ -604,4 +605,17 @@ public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput, return null; } + + /** + * Returns the {@link RulesEngine} associated with this editor + * + * @return the {@link RulesEngine} associated with this editor, or null + */ + public RulesEngine getRulesEngine() { + if (mGraphicalEditor instanceof GraphicalEditorPart) { + return ((GraphicalEditorPart) mGraphicalEditor).getRulesEngine(); + } + + return null; + } } 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 2da2467..303c58d 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 @@ -335,7 +335,7 @@ public class CanvasViewInfo implements IPropertySource { String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor()); String parentFqcn = null; - Rect bounds = new Rect(getAbsRect()); + Rect bounds = SwtUtils.toRect(getAbsRect()); Rect parentBounds = null; UiElementNode uiParent = uiNode.getUiParent(); @@ -343,7 +343,7 @@ public class CanvasViewInfo implements IPropertySource { parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor()); } if (getParent() != null) { - parentBounds = new Rect(getParent().getAbsRect()); + parentBounds = SwtUtils.toRect(getParent().getAbsRect()); } SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds); 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 66f2327..a514447 100755 --- 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 @@ -179,6 +179,9 @@ import java.util.regex.Pattern; contrib = createDynamicChoices( (MenuAction.Choices)firstAction, choiceMap, actionsMap); } + } else { + // Must be a plain action + contrib = createDynamicAction(firstAction, actionsMap); } if (contrib != null) { @@ -328,7 +331,7 @@ import java.util.regex.Pattern; mEditor.wrapUndoEditXmlModel(label, new Runnable() { public void run() { - // Invoke the closures of all the actions using the same action-id + // Invoke the callbacks of all the actions using the same action-id for (MenuAction a2 : actions) { if (a2 instanceof MenuAction.Action) { IMenuCallback c = ((MenuAction.Action) a2).getCallback(); @@ -354,6 +357,66 @@ import java.util.regex.Pattern; /** * Invoked by {@link #populateDynamicContextMenu()} to create a new menu item + * for a plain action. This is nearly identical to {@link #createDynamicMenuToggle}, + * except for the {@link IAction} type and the removal of setChecked, isChecked, etc. + * + * @param firstAction The action to convert to a menu item. In the case of a + * multiple selection, this is the first of many similar actions. + * @param actionsMap Map of all contributed actions. + * @return a new {@link IContributionItem} to add to the context menu + */ + private IContributionItem createDynamicAction( + final MenuAction.Action firstAction, + final TreeMap<String, ArrayList<MenuAction>> actionsMap) { + + Action a = new Action(firstAction.getTitle(), IAction.AS_PUSH_BUTTON) { + @Override + public void run() { + final List<MenuAction> actions = actionsMap.get(firstAction.getId()); + if (actions == null || actions.isEmpty()) { + return; + } + + String label = actions.get(0).getTitle(); + if (actions.size() > 1) { + label += String.format(" (%d elements)", actions.size()); + } + + if (mEditor.isEditXmlModelPending()) { + // This should not be happening. + logError("Action '%s' failed: XML changes pending, document might be corrupt.", //$NON-NLS-1$ + label); + return; + } + + mEditor.wrapUndoEditXmlModel(label, new Runnable() { + public void run() { + // Invoke the callbacks of all the actions using the same action-id + for (MenuAction a2 : actions) { + if (a2 instanceof MenuAction.Action) { + IMenuCallback c = ((MenuAction.Action) a2).getCallback(); + if (c != null) { + try { + // Values do not apply for plain actions + c.action(a2, null /* valueId */, null /* newValue */); + } catch (Exception e) { + RulesEngine gre = mCanvas.getRulesEngine(); + gre.logError("XML edit operation failed: %s", e.toString()); + } + } + } + } + } + }); + } + }; + a.setId(firstAction.getId()); + + return new ActionContributionItem(a); + } + + /** + * Invoked by {@link #populateDynamicContextMenu()} to create a new menu item * for a {@link MenuAction.Choices}. * <p/> * Multiple-choices are represented by a sub-menu containing checked items. @@ -429,7 +492,7 @@ import java.util.regex.Pattern; } // We consider the item to be checked if all actions are all checked. - // This means a mixed item will be first toggled from off to on by all the closures. + // This means a mixed item will be first toggled from off to on by all the callbacks. final boolean isChecked = numOff == 0 && numOn > 0; boolean isMixed = numOff > 0 && numOn > 0; @@ -456,7 +519,7 @@ import java.util.regex.Pattern; mEditor.wrapUndoEditXmlModel(label, new Runnable() { public void run() { - // Invoke the closures of all the actions using the same action-id + // Invoke the callbacks of all the actions using the same action-id for (MenuAction a2 : actions) { if (a2 instanceof MenuAction.Action) { try { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java index 73711aa..728c089 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java @@ -899,7 +899,7 @@ public class GraphicalEditorPart extends EditorPart } if (mRulesEngine == null) { - mRulesEngine = new RulesEngine(mEditedFile.getProject()); + mRulesEngine = new RulesEngine(this, mEditedFile.getProject()); if (mCanvasViewer != null) { mCanvasViewer.getCanvas().setRulesEngine(mRulesEngine); } @@ -949,6 +949,15 @@ public class GraphicalEditorPart extends EditorPart return mLayoutEditor; } + /** + * Returns the {@link RulesEngine} associated with this editor + * + * @return the {@link RulesEngine} associated with this editor, never null + */ + public RulesEngine getRulesEngine() { + return mRulesEngine; + } + /* package */ LayoutCanvas getCanvasControl() { if (mCanvasViewer != null) { return mCanvasViewer.getCanvas(); @@ -1174,7 +1183,7 @@ public class GraphicalEditorPart extends EditorPart Set<UiElementNode> explodeNodes = canvas.getNodesToExplode(); // Compute the layout - Rectangle rect = getBounds(); + Rectangle rect = getScreenBounds(); int width = rect.width; int height = rect.height; @@ -1355,7 +1364,7 @@ public class GraphicalEditorPart extends EditorPart // FIXME: get rid of the current LayoutScene if any. } - public Rectangle getBounds() { + public Rectangle getScreenBounds() { return mConfigComposite.getScreenBounds(); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java index c11d00b..d36b96c 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java @@ -17,6 +17,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import com.android.ide.common.api.DropFeedback; import com.android.ide.common.api.INode; +import com.android.ide.common.api.InsertType; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; import com.android.ide.eclipse.adt.AdtPlugin; @@ -316,10 +317,19 @@ public class MoveGesture extends DropGesture { String label = computeUndoLabel(mTargetNode, elements, event.detail); mCanvas.getLayoutEditor().wrapUndoEditXmlModel(label, new Runnable() { public void run() { + InsertType insertType; + if (event.detail == DND.DROP_MOVE) { + insertType = InsertType.MOVE; + } else if (GlobalCanvasDragInfo.getInstance().getSourceCanvas() != null) { + insertType = InsertType.PASTE; + } else { + insertType = InsertType.CREATE; + } mCanvas.getRulesEngine().callOnDropped(mTargetNode, elementsFinal, mFeedback, - new Point(canvasPoint.x, canvasPoint.y)); + new Point(canvasPoint.x, canvasPoint.y), + insertType); // Clean up drag if applicable if (event.detail == DND.DROP_MOVE) { GlobalCanvasDragInfo.getInstance().removeSource(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteComposite.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteComposite.java index c249981..6b46688 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteComposite.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteComposite.java @@ -16,15 +16,21 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import com.android.ide.common.api.InsertType; import com.android.ide.common.api.Rect; +import com.android.ide.common.layout.BaseView; import com.android.ide.common.layoutlib.LayoutLibrary; import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.IGraphicalLayoutEditor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutConstants; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; +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.ide.eclipse.adt.internal.sdk.AndroidTargetData; @@ -60,10 +66,13 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.ScrollBar; +import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.StringWriter; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; @@ -548,11 +557,18 @@ public class PaletteComposite extends Composite { // not to make it much larger than necessary since to crop this we rely on // actually scanning pixels. - /** Width of the rendered preview image (before it is cropped) */ - private static final int RENDER_HEIGHT = 400; + /** + * Width of the rendered preview image (before it is cropped), although the actual + * width may be smaller (since we also take the device screen's size into account) + */ + private static final int MAX_RENDER_HEIGHT = 400; - /** Height of the rendered preview image (before it is cropped) */ - private static final int RENDER_WIDTH = 500; + /** + * Height of the rendered preview image (before it is cropped), although the + * actual width may be smaller (since we also take the device screen's size into + * account) + */ + private static final int MAX_RENDER_WIDTH = 500; /** Amount of alpha to multiply into the image (divided by 256) */ private static final int IMG_ALPHA = 216; @@ -655,6 +671,8 @@ public class PaletteComposite extends Composite { Document document = null; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); try { + factory.setNamespaceAware(true); + factory.setValidating(false); DocumentBuilder builder = factory.newDocumentBuilder(); document = builder.newDocument(); } catch (ParserConfigurationException e) { @@ -668,6 +686,13 @@ public class PaletteComposite extends Composite { String viewName = desc.getXmlLocalName(); Element element = document.createElement(viewName); + + // Set up a proper name space + Attr attr = document.createAttributeNS(XmlnsAttributeDescriptor.XMLNS_URI, + "xmlns:android"); //$NON-NLS-1$ + attr.setValue(BaseView.ANDROID_URI); + element.getAttributes().setNamedItemNS(attr); + element.setAttributeNS(SdkConstants.NS_RESOURCES, LayoutConstants.ATTR_LAYOUT_WIDTH, LayoutConstants.VALUE_WRAP_CONTENT); element.setAttributeNS(SdkConstants.NS_RESOURCES, @@ -679,15 +704,37 @@ public class PaletteComposite extends Composite { String text = DescriptorsUtils.getFreeWidgetId(uiRoot, viewName); element.setAttributeNS(SdkConstants.NS_RESOURCES, LayoutConstants.ATTR_TEXT, text); - document.insertBefore(element, null); + document.appendChild(element); // Construct UI model from XML - DocumentDescriptor documentDescriptor = - new DocumentDescriptor("layout_doc", null); //$NON-NLS-1$ - UiDocumentNode model = new UiDocumentNode(documentDescriptor); + AndroidTargetData data = layoutEditor.getTargetData(); + DocumentDescriptor documentDescriptor; + if (data == null) { + documentDescriptor = new DocumentDescriptor("temp", null /*children*/); //$NON-NLS-1$ + } else { + documentDescriptor = data.getLayoutDescriptors().getDescriptor(); + } + UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode(); model.setEditor(layoutEditor); + model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider()); model.loadFromXmlNode(document); + // Call the create-hooks such that we for example insert mandatory + // children into views like the DialerFilter, apply image source attributes + // to ImageButtons, etc. + if (editor instanceof GraphicalEditorPart) { + LayoutCanvas canvas = ((GraphicalEditorPart) editor).getCanvasControl(); + NodeFactory nodeFactory = canvas.getNodeFactory(); + UiElementNode parent = model.getUiRoot(); + UiElementNode child = parent.getUiChildren().get(0); + if (child instanceof UiViewElementNode) { + UiViewElementNode childUiNode = (UiViewElementNode) child; + NodeProxy childNode = nodeFactory.create(childUiNode); + canvas.getRulesEngine().callCreateHooks(layoutEditor, + null, childNode, InsertType.CREATE); + } + } + boolean hasTransparency = false; LayoutLibrary layoutLibrary = editor.getLayoutLibrary(); if (layoutLibrary != null) { @@ -697,8 +744,23 @@ public class PaletteComposite extends Composite { } } - LayoutScene scene = editor.render(model, RENDER_WIDTH, RENDER_HEIGHT, + LayoutScene scene = null; + try { + // Use at most the size of the screen for the preview render. + // This is important since when we fill the size of certain views (like + // a SeekBar), we want it to at most be the width of the screen, and for small + // screens the RENDER_WIDTH was wider. + org.eclipse.draw2d.geometry.Rectangle screenBounds = editor.getScreenBounds(); + int renderWidth = Math.min(screenBounds.width, MAX_RENDER_WIDTH); + int renderHeight = Math.min(screenBounds.height, MAX_RENDER_HEIGHT); + + scene = editor.render(model, renderWidth, renderHeight, null /* explodeNodes */, hasTransparency); + } catch (Throwable t) { + // Previews can fail for a variety of reasons -- let's not bug + // the user with it + return null; + } if (scene != null) { if (scene.getResult() == SceneResult.SUCCESS) { @@ -761,6 +823,41 @@ public class PaletteComposite extends Composite { } /** + * Utility method to print out the contents of the given XML document. This is + * really useful when working on the preview code above. I'm including all the + * code inside a constant false, which means the compiler will omit all the code, + * but I'd like to leave it in the code base and by doing it this way rather than + * as commented out code the code won't be accidentally broken. + */ + @SuppressWarnings("all") + private static void dumpDocument(Document document) { + // Diagnostics: print out the XML that we're about to render + if (false) { // Will be omitted by the compiler + org.apache.xml.serialize.OutputFormat outputFormat = + new org.apache.xml.serialize.OutputFormat( + "XML", "ISO-8859-1", true); //$NON-NLS-1$ //$NON-NLS-2$ + outputFormat.setIndent(2); + outputFormat.setLineWidth(100); + outputFormat.setIndenting(true); + outputFormat.setOmitXMLDeclaration(true); + outputFormat.setOmitDocumentType(true); + StringWriter stringWriter = new StringWriter(); + // Using FQN here to avoid having an import above, which will result + // in a deprecation warning, and there isn't a way to annotate a single + // import element with a SuppressWarnings. + org.apache.xml.serialize.XMLSerializer serializer = + new org.apache.xml.serialize.XMLSerializer(stringWriter, outputFormat); + serializer.setNamespaces(true); + try { + serializer.serialize(document.getDocumentElement()); + System.out.println(stringWriter.toString()); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** * Returns the image shown as the drag source effect. The image may be a preview * of the palette item, or just a placeholder image; {@link #isPlaceholder()} can * tell the difference. diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java index 6c55dd3..c2c46f1 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java @@ -15,9 +15,12 @@ */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import com.android.ide.common.api.Rect; + import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Display; import java.awt.image.BufferedImage; @@ -109,4 +112,38 @@ public class SwtUtils { return awtImage; } + + /** + * Converts the given SWT {@link Rectangle} into an ADT {@link Rect} + * + * @param swtRect the SWT {@link Rectangle} + * @return an equivalent {@link Rect} + */ + public static Rect toRect(Rectangle swtRect) { + return new Rect(swtRect.x, swtRect.y, swtRect.width, swtRect.height); + } + + /** + * Sets the values of the given ADT {@link Rect} to the values of the given SWT + * {@link Rectangle} + * + * @param target the ADT {@link Rect} to modify + * @param source the SWT {@link Rectangle} to read values from + */ + public static void set(Rect target, Rectangle source) { + target.set(source.x, source.y, source.width, source.height); + } + + /** + * Compares an ADT {@link Rect} with an SWT {@link Rectangle} and returns true if they + * are equivalent + * + * @param r1 the ADT {@link Rect} + * @param r2 the SWT {@link Rectangle} + * @return true if the two rectangles are equivalent + */ + public static boolean equals(Rect r1, Rectangle r2) { + return r1.x == r2.x && r1.y == r2.y && r1.w == r2.width && r1.h == r2.height; + + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java index 825a0e4..e608377 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java @@ -18,6 +18,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gre; import com.android.ide.common.api.INode; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import org.eclipse.swt.graphics.Rectangle; @@ -67,7 +68,7 @@ public class NodeFactory { proxy = new NodeProxy(uiNode, bounds, this); mNodeMap.put(uiNode, proxy); - } else if (bounds != null && !proxy.getBounds().equals(bounds)) { + } else if (bounds != null && !SwtUtils.equals(proxy.getBounds(), bounds)) { // Update the bounds if necessary proxy.setBounds(bounds); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java index 3916eaa..59c6e15 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java @@ -29,6 +29,7 @@ import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescripto 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.gle2.SimpleAttribute; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; @@ -71,7 +72,7 @@ public class NodeProxy implements INode { if (bounds == null) { mBounds = new Rect(); } else { - mBounds = new Rect(bounds); + mBounds = SwtUtils.toRect(bounds); } } @@ -85,7 +86,7 @@ public class NodeProxy implements INode { * This is a package-protected method, only the {@link NodeFactory} uses this method. */ /*package*/ void setBounds(Rectangle bounds) { - mBounds.set(bounds); + SwtUtils.set(mBounds, bounds); } /** @@ -195,38 +196,14 @@ public class NodeProxy implements INode { } public INode appendChild(String viewFqcn) { - checkEditOK(); - - // Find the descriptor for this FQCN - ViewElementDescriptor vd = getFqcnViewDescriptor(viewFqcn); - if (vd == null) { - warnPrintf("Can't create a new %s element", viewFqcn); - return null; - } - - // Append at the end. - UiElementNode uiNew = mNode.appendNewUiChild(vd); - - // TODO we probably want to defer that to the GRE to use IViewRule#getDefaultAttributes() - DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/); - - Node xmlNode = uiNew.createXmlNode(); - - if (!(uiNew instanceof UiViewElementNode) || xmlNode == null) { - // Both things are not supposed to happen. When they do, we're in big trouble. - // We don't really know how to revert the state at this point and the UI model is - // now out of sync with the XML model. - // Panic ensues. - // The best bet is to abort now. The edit wrapper will release the edit and the - // XML/UI should get reloaded properly (with a likely invalid XML.) - warnPrintf("Failed to create a new %s element", viewFqcn); - throw new RuntimeException("XML node creation failed."); //$NON-NLS-1$ - } - - return mFactory.create((UiViewElementNode) uiNew); + return insertOrAppend(viewFqcn, -1); } public INode insertChildAt(String viewFqcn, int index) { + return insertOrAppend(viewFqcn, index); + } + + private INode insertOrAppend(String viewFqcn, int index) { checkEditOK(); // Find the descriptor for this FQCN @@ -236,13 +213,18 @@ public class NodeProxy implements INode { return null; } - // Insert at the requested position or at the end. - int n = mNode.getUiChildren().size(); - UiElementNode uiNew = null; - if (index < 0 || index >= n) { + final UiElementNode uiNew; + if (index == -1) { + // Append at the end. uiNew = mNode.appendNewUiChild(vd); } else { - uiNew = mNode.insertNewUiChild(index, vd); + // Insert at the requested position or at the end. + int n = mNode.getUiChildren().size(); + if (index < 0 || index >= n) { + uiNew = mNode.appendNewUiChild(vd); + } else { + uiNew = mNode.insertNewUiChild(index, vd); + } } // TODO we probably want to defer that to the GRE to use IViewRule#getDefaultAttributes() @@ -261,7 +243,18 @@ public class NodeProxy implements INode { throw new RuntimeException("XML node creation failed."); //$NON-NLS-1$ } - return mFactory.create((UiViewElementNode) uiNew); + UiViewElementNode uiNewView = (UiViewElementNode) uiNew; + NodeProxy newNode = mFactory.create(uiNewView); + + AndroidXmlEditor editor = mNode.getEditor(); + if (editor instanceof LayoutEditor) { + RulesEngine engine = ((LayoutEditor)editor).getRulesEngine(); + if (engine != null) { + engine.callCreateHooks(editor, this, newNode, null); + } + } + + return newNode; } public boolean setAttribute(String uri, String name, String value) { 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 7856cd9..a14d812 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 @@ -22,12 +22,16 @@ import com.android.ide.common.api.IDragElement; import com.android.ide.common.api.IGraphics; 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.InsertType; import com.android.ide.common.api.MenuAction; import com.android.ide.common.api.Point; import com.android.ide.common.layout.ViewRule; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.IGraphicalLayoutEditor; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleElement; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; @@ -63,6 +67,15 @@ import java.util.Map; public class RulesEngine { private final IProject mProject; private final Map<Object, IViewRule> mRulesCache = new HashMap<Object, IViewRule>(); + /** + * The type of any upcoming node manipulations performed by the {@link IViewRule}s. + * When actions are performed in the tool (like a paste action, or a drag from palette, + * or a drag move within the canvas, etc), these are different types of inserts, + * and we don't want to have the rules track them closely (and pass them back to us + * in the {@link INode#insertChildAt} methods etc), so instead we track the state + * here on behalf of the currently executing rule. + */ + private InsertType mInsertType = InsertType.CREATE; /** * Class loader (or null) used to load user/project-specific IViewRule @@ -77,14 +90,21 @@ public class RulesEngine { private boolean mUserClassLoaderInited; /** + * The editor which owns this {@link RulesEngine} + */ + private IGraphicalLayoutEditor mEditor; + + /** * Creates a new {@link RulesEngine} associated with the selected project. * <p/> * The rules engine will look in the project for a tools jar to load custom view rules. * + * @param editor the editor which owns this {@link RulesEngine} * @param project A non-null open project. */ - public RulesEngine(IProject project) { + public RulesEngine(IGraphicalLayoutEditor editor, IProject project) { mProject = project; + mEditor = editor; } /** @@ -194,6 +214,7 @@ public class RulesEngine { if (rule != null) { try { + mInsertType = InsertType.CREATE; return rule.getContextMenu(selectedNode); } catch (Exception e) { @@ -335,12 +356,14 @@ public class RulesEngine { public void callOnDropped(NodeProxy targetNode, IDragElement[] elements, DropFeedback feedback, - Point where) { + Point where, + InsertType insertType) { // try to find a rule for this element's FQCN IViewRule rule = loadRule(targetNode.getNode()); if (rule != null) { try { + mInsertType = insertType; rule.onDropped(targetNode, elements, feedback, where); } catch (Exception e) { @@ -379,6 +402,7 @@ public class RulesEngine { if (rule != null) { try { + mInsertType = InsertType.PASTE; rule.onPaste(targetNode, pastedElements); } catch (Exception e) { @@ -389,6 +413,74 @@ public class RulesEngine { } } + // ---- Creation customizations ---- + + /** + * Invokes the create hooks ({@link IViewRule#onCreate}, + * {@link IViewRule#onChildInserted} when a new child has been created/pasted/moved, and + * is inserted into a given parent. The parent may be null (for example when rendering + * top level items for preview). + * + * @param editor the XML editor to apply edits to the model for (performed by view + * rules) + * @param parentNode the parent XML node, or null if unknown + * @param childNode the XML node of the new node, never null + * @param overrideInsertType If not null, specifies an explicit insert type to use for + * edits made during the customization + */ + public void callCreateHooks( + AndroidXmlEditor editor, + NodeProxy parentNode, NodeProxy childNode, + InsertType overrideInsertType) { + IViewRule parentRule = null; + + if (parentNode != null) { + UiViewElementNode parentUiNode = parentNode.getNode(); + parentRule = loadRule(parentUiNode); + } + + if (overrideInsertType != null) { + mInsertType = overrideInsertType; + } + + UiViewElementNode newUiNode = childNode.getNode(); + IViewRule childRule = loadRule(newUiNode); + if (childRule != null || parentRule != null) { + callCreateHooks(editor, mInsertType, parentRule, parentNode, + childRule, childNode); + } + } + + private static void callCreateHooks( + final AndroidXmlEditor editor, final InsertType insertType, + final IViewRule parentRule, final INode parentNode, + final IViewRule childRule, final INode newNode) { + // Notify the parent about the new child in case it wants to customize it + // (For example, a ScrollView parent can go and set all its children's layout params to + // fill the parent.) + if (!editor.isEditXmlModelPending()) { + editor.wrapUndoEditXmlModel("Customize creation", new Runnable() { + public void run() { + callCreateHooks(editor, insertType, + parentRule, parentNode, childRule, newNode); + } + }); + return; + } + + if (parentRule != null) { + parentRule.onChildInserted(newNode, parentNode, insertType); + } + + // Look up corresponding IViewRule, and notify the rule about + // this create action in case it wants to customize the new object. + // (For example, a rule for TabHosts can go and create a default child tab + // when you create it.) + if (childRule != null) { + childRule.onCreate(newNode, parentNode, insertType); + } + } + // ---- private --- /** @@ -531,7 +623,11 @@ public class RulesEngine { // than in the same package as the widgets. String ruleClassName; ClassLoader classLoader; - if (realFqcn.startsWith("android.")) { //$NON-NLS-1$ + if (realFqcn.startsWith("android.") || //$NON-NLS-1$ + // FIXME: Remove this special case as soon as we pull + // the MapViewRule out of this code base and bundle it + // with the add ons + realFqcn.startsWith("com.google.android.maps.")) { //$NON-NLS-1$ // This doesn't handle a case where there are name conflicts // (e.g. where there are multiple different views with the same // class name and only differing in package names, but that's a @@ -697,6 +793,9 @@ public class RulesEngine { } return null; } - } + public IViewMetadata getMetadata(final String fqcn) { + return new ViewMetadata(mEditor.getLayoutEditor(), fqcn); + } + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadata.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadata.java new file mode 100644 index 0000000..eeebad7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadata.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2010 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.gre; + +/** + * An implementation of {@link IViewMetadata} which consults the + * SDK descriptors to answer metadata questions about views. + */ +import com.android.ide.common.api.IViewMetadata; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.IGraphicalLayoutEditor; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; + +import java.util.HashMap; +import java.util.Map; + +final class ViewMetadata implements IViewMetadata { + /** The {@link ElementDescriptor} for this view, computed lazily */ + private ElementDescriptor mDescriptor; + + /** The fully qualified class name of the view whose metadata this class represents */ + private String mFqcn; + + /** The {@link IGraphicalLayoutEditor} associated with the view we're looking up */ + private LayoutEditor mEditor; + + /** + * A map from class names to {@link FillPreference} which indicates how each view + * prefers to grow when added in various layout contexts + */ + private static final Map<String,FillPreference> mFill = new HashMap<String,FillPreference>(); + static { + // Hardcoded metadata about fill preferences for various known views. We should + // work to try to get this into the platform as designtime annotations. + + mFill.put("android.widget.EditText", FillPreference.WIDTH_IN_VERTICAL); //$NON-NLS-1$ + mFill.put("android.widget.DialerFilter", FillPreference.WIDTH_IN_VERTICAL); //$NON-NLS-1$ + mFill.put("android.widget.SeekBar", FillPreference.WIDTH_IN_VERTICAL); //$NON-NLS-1$ + mFill.put("android.widget.Spinner", FillPreference.WIDTH_IN_VERTICAL); //$NON-NLS-1$ + mFill.put("android.widget.AutoComplete", FillPreference.WIDTH_IN_VERTICAL); //$NON-NLS-1$ + mFill.put("android.widget.ListView", FillPreference.WIDTH_IN_VERTICAL); //$NON-NLS-1$ + mFill.put("android.widget.GridView", FillPreference.OPPOSITE); //$NON-NLS-1$ + mFill.put("android.widget.Gallery", FillPreference.WIDTH_IN_VERTICAL); //$NON-NLS-1$ + mFill.put("android.widget.TabWidget", FillPreference.WIDTH_IN_VERTICAL); //$NON-NLS-1$ + mFill.put("android.widget.MapView", FillPreference.OPPOSITE); //$NON-NLS-1$ + mFill.put("android.widget.WebView", FillPreference.OPPOSITE); //$NON-NLS-1$ + + // In addition, all layouts are FillPreference.OPPOSITE - these are computed + // lazily rather than enumerating them here + + // For any other view, the fallback fill preference is FillPreference.NONE. + } + + public ViewMetadata(LayoutEditor editor, String fqcn) { + super(); + mFqcn = fqcn; + mEditor = editor; + } + + /** Lazily look up the descriptor for the FQCN of this metadata object */ + private boolean findDescriptor() { + if (mDescriptor == null) { + // Look up the corresponding view element node. We don't need the graphical part; + // we just need the project context. Maybe I should extract this code into + // a utility. + mDescriptor = mEditor.getFqcnViewDescriptor(mFqcn); + } + + return mDescriptor != null; + } + + public boolean isParent() { + if (findDescriptor()) { + return mDescriptor.hasChildren(); + } + + return false; + } + + public String getDisplayName() { + if (findDescriptor()) { + return mDescriptor.getUiName(); + } + + return mFqcn.substring(mFqcn.lastIndexOf('.') + 1); // This also works when there is no "." + } + + public String getTooltip() { + if (findDescriptor()) { + return mDescriptor.getTooltip(); + } + + return null; + } + + /** Returns true if this view represents a layout */ + private boolean isLayout() { + if (findDescriptor()) { + return mDescriptor.hasChildren(); + } + return false; + } + + public FillPreference getFillPreference() { + FillPreference fillPreference = mFill.get(mFqcn); + if (fillPreference == null) { + if (isLayout()) { + fillPreference = FillPreference.OPPOSITE; + } else { + fillPreference = FillPreference.NONE; + } + mFill.put(mFqcn, fillPreference); + } + + return fillPreference; + } +} |