aboutsummaryrefslogtreecommitdiffstats
path: root/eclipse/plugins
diff options
context:
space:
mode:
authorRaphael Moll <ralf@android.com>2010-09-01 16:34:51 -0700
committerAndroid Code Review <code-review@android.com>2010-09-01 16:34:51 -0700
commit7e67b9023d75320615d8a1f8472b9d0d0171b6be (patch)
tree171ccd421e336a2e24f5746355a169c3c9c071a0 /eclipse/plugins
parentf0dad1edb62d1e56848113ec1164cb76ac0edc12 (diff)
parent44a39167e3f9ee89e495716b7c3f0094ab91f2b1 (diff)
downloadsdk-7e67b9023d75320615d8a1f8472b9d0d0171b6be.zip
sdk-7e67b9023d75320615d8a1f8472b9d0d0171b6be.tar.gz
sdk-7e67b9023d75320615d8a1f8472b9d0d0171b6be.tar.bz2
Merge changes I682ba19d,Ie7451c21
* changes: GLE2 context menu: Handle boolean properties as tri-state. GLE2 context menu support.
Diffstat (limited to 'eclipse/plugins')
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseLayout.groovy4
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseView.groovy256
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.view.View.groovy3
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.LinearLayout.groovy31
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.RelativeLayout.groovy2
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/INode.java26
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IViewRule.java20
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/MenuAction.java411
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java371
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java2
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java16
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java53
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java1
13 files changed, 1116 insertions, 80 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseLayout.groovy b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseLayout.groovy
index ab380cb..0d2ca50 100755
--- a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseLayout.groovy
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseLayout.groovy
@@ -16,6 +16,7 @@
package com.android.adt.gscripts;
+
public class BaseLayout extends BaseView {
public boolean onInitialize(String fqcn) {
@@ -26,6 +27,9 @@ public class BaseLayout extends BaseView {
super.onDispose();
}
+ public List<MenuAction> getContextMenu(INode selectedNode) {
+ return super.getContextMenu(selectedNode);
+ }
// ==== Paste support ====
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseView.groovy b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseView.groovy
index 714c6b7..1ddf717 100755
--- a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseView.groovy
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseView.groovy
@@ -16,8 +16,17 @@
package com.android.adt.gscripts;
+/**
+ * Common IViewRule processing to all view and layout classes.
+ */
public class BaseView implements IViewRule {
+ /**
+ * Namespace for the Android resource XML,
+ * i.e. "http://schemas.android.com/apk/res/android"
+ */
+ public static String ANDROID_URI = "http://schemas.android.com/apk/res/android";
+
// 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";
@@ -27,15 +36,12 @@ public class BaseView implements IViewRule {
// Some common Android layout attribute values used by the view rules.
public static String VALUE_FILL_PARENT = "fill_parent";
- public static String VALUE_MATCH_PARENT = "match_parent";
- public static String VALUE_MATCH_CONTENT = "match_content";
+ public static String VALUE_MATCH_PARENT = "match_parent"; // like fill_parent for API 8
+ public static String VALUE_WRAP_CONTENT = "wrap_content";
-
- /**
- * Namespace for the Android resource XML,
- * i.e. "http://schemas.android.com/apk/res/android"
- */
- public static String ANDROID_URI = "http://schemas.android.com/apk/res/android";
+ // Cache of attributes. Key is FQCN of a node mixed with its view hierarchy parent.
+ // Values are a custom map as needed by getContextMenu.
+ public Map mAttributesMap = [:];
public boolean onInitialize(String fqcn) {
// This base rule can handle any class so we don't need to filter on FQCN.
@@ -63,6 +69,240 @@ public class BaseView implements IViewRule {
return null;
}
+ // === Context Menu ===
+
+ /**
+ * Generate custom actions for the context menu: <br/>
+ * - Explicit layout_width and layout_height attributes.
+ * - List of all other simple toggle attributes.
+ */
+ public List<MenuAction> getContextMenu(INode selectedNode) {
+
+ // Compute the key for mAttributesMap. This depends on the type of this node and
+ // its parent in the view hierarchy.
+ def key = selectedNode.getFqcn() + "_";
+ def parent = selectedNode.getParent();
+ if (parent) key = key + parent.getFqcn();
+
+ def custom_w = "Custom...";
+ def curr_w = selectedNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WIDTH);
+
+ if (curr_w == VALUE_FILL_PARENT) {
+ curr_w = VALUE_MATCH_PARENT;
+ } else if (curr_w != VALUE_WRAP_CONTENT && curr_w != VALUE_MATCH_PARENT) {
+ curr_w = "zcustom";
+ if (!!curr_w) {
+ custom_w = "Custom: ${curr_w}"
+ }
+ }
+
+ def custom_h = "Custom...";
+ def curr_h = selectedNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
+
+ if (curr_h == VALUE_FILL_PARENT) {
+ curr_h = VALUE_MATCH_PARENT;
+ } else if (curr_h != VALUE_WRAP_CONTENT && curr_h != VALUE_MATCH_PARENT) {
+ curr_h = "zcustom";
+ if (!!curr_h) {
+ custom_h = "Custom: ${curr_h}"
+ }
+ }
+
+ def onChange = { MenuAction.Action action, String valueId, Boolean newValue ->
+ def actionId = action.getId();
+ def node = selectedNode;
+
+ switch (actionId) {
+ case "layout_1width":
+ if (!valueId.startsWith("z")) {
+ node.editXml("Change attribute " + ATTR_LAYOUT_WIDTH) {
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, valueId);
+ }
+ }
+ return;
+ case "layout_2height":
+ if (!valueId.startsWith("z")) {
+ node.editXml("Change attribute " + ATTR_LAYOUT_HEIGHT) {
+ node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, valueId);
+ }
+ }
+ return;
+ }
+
+ if (actionId.startsWith("@prop@")) {
+ actionId = actionId.substring(6);
+
+ def props = mAttributesMap[key];
+ def prop = props?.get(actionId);
+
+ if (prop) {
+ node.editXml("Change attribute " + actionId) {
+ if (prop.isToggle) {
+ // case of toggle
+ String value = "";
+ switch(valueId) {
+ case "1t":
+ value = newValue ? "true" : "";
+ break;
+ case "2f":
+ value = newValue ? "false" : "";
+ break;
+ }
+ node.setAttribute(ANDROID_URI, actionId, value);
+ } else if (prop.isFlag) {
+ // case of a flag
+ def values = "";
+ if (valueId != "~2clr") {
+ values = node.getStringAttr(ANDROID_URI, actionId);
+ if (!values) {
+ values = [] as Set;
+ } else {
+ values = ([] as Set) + (values.split("\\|") as Set);
+ }
+ if (newValue) {
+ values << valueId;
+ } else {
+ values = values - valueId;
+ }
+ values = values.join("|");
+ }
+ node.setAttribute(ANDROID_URI, actionId, values);
+ } else {
+ // case of an enum
+ def value = "";
+ if (valueId != "~2clr") {
+ value = newValue ? valueId : "";
+ }
+ node.setAttribute(ANDROID_URI, actionId, value);
+ }
+ }
+ }
+ }
+ }
+
+ def list1 = [
+ new MenuAction.Choices("layout_1width", "Layout Width",
+ [ "wrap_content": "Wrap Content",
+ "match_parent": "Match Parent",
+ "zcustom": custom_w ],
+ curr_w,
+ onChange ),
+ new MenuAction.Choices("layout_2height", "Layout Height",
+ [ "wrap_content": "Wrap Content",
+ "match_parent": "Match Parent",
+ "zcustom": custom_h ],
+ curr_h,
+ onChange ),
+ new MenuAction.Group("properties", "Properties"),
+ ];
+
+ // Prepare a list of all simple properties.
+
+ def props = mAttributesMap[key];
+ if (props == null) {
+ // Prepare the property map
+ props = [:]
+ for (attrInfo in selectedNode.getDeclaredAttributes()) {
+ def id = attrInfo?.getName();
+ if (id == null || id == ATTR_LAYOUT_WIDTH || id == ATTR_LAYOUT_HEIGHT) {
+ // Layout width/height are already handled at the root level
+ continue;
+ }
+ def formats = attrInfo?.getFormats();
+ if (formats == null) {
+ continue;
+ }
+
+ def title = prettyName(id);
+
+ if (IAttributeInfo.Format.BOOLEAN in formats) {
+ props[id] = [ isToggle: true, title: title ];
+
+ } else if (IAttributeInfo.Format.ENUM in formats) {
+ // Convert each enum into a map id=>title
+ def values = [:];
+ attrInfo.getEnumValues().each { e -> values[e] = prettyName(e) }
+
+ props[id]= [ isToggle: false,
+ isFlag: false,
+ title: title,
+ choices: values ];
+
+ } else if (IAttributeInfo.Format.FLAG in formats) {
+ // Convert each flag into a map id=>title
+ def values = [:];
+ attrInfo.getFlagValues().each { e -> values[e] = prettyName(e) };
+
+ props[id] = [ isToggle: false,
+ isFlag: true,
+ title: title,
+ choices: values ];
+ }
+ }
+ mAttributesMap[key] = props;
+ }
+
+ def list2 = [];
+
+ props.each { id, p ->
+ def a = null;
+ if (p.isToggle) {
+ // Toggles are handled as a multiple-choice between true, false and nothing (clear)
+ def value = selectedNode.getStringAttr(ANDROID_URI, id);
+ if (value != null) value = value.toLowerCase();
+ switch(value) {
+ case "true":
+ value = "1t";
+ break;
+ case "false":
+ value = "2f";
+ break;
+ default:
+ value = "4clr";
+ break;
+ }
+
+ a = new MenuAction.Choices(
+ "@prop@" + id,
+ p.title,
+ [ "1t": "True",
+ "2f": "False",
+ "3sep": MenuAction.Choices.SEPARATOR,
+ "4clr": "Clear" ],
+ value,
+ "properties",
+ onChange);
+ } else {
+ // Enum or flags. Their possible values are the multiple-choice items,
+ // with an extra "clear" option to remove everything.
+ def current = selectedNode.getStringAttr(ANDROID_URI, id);
+ if (!current) {
+ current = "~2clr";
+ }
+ a = new MenuAction.Choices(
+ "@prop@" + id,
+ p.title,
+ p.choices + [ "~1sep": MenuAction.Choices.SEPARATOR,
+ "~2clr": "Clear " + (p.isFlag ? "flag" : "enum") ],
+ current,
+ "properties",
+ onChange);
+ }
+ if (a) list2.add(a);
+ }
+
+ return list1 + list2;
+ }
+
+ public String prettyName(String name) {
+ if (name) {
+ def c = name[0];
+ name = name.replaceFirst(c, c.toUpperCase());
+ name = name.replace("_", " ");
+ }
+ return name;
+ }
+
// ==== Selection ====
public void onSelected(IGraphics gc, INode selectedNode,
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.view.View.groovy b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.view.View.groovy
index 98f45e5..c4b982b 100755
--- a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.view.View.groovy
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.view.View.groovy
@@ -19,6 +19,9 @@ package com.android.adt.gscripts;
/**
* An {@link IViewRule} for android.view.View and all its derived classes.
* This is the "root" rule, that is used whenever there is not more specific rule to apply.
+ * <p/>
+ * There is no customization here, everything that is common to all views is simply
+ * implemented in BaseView.
*/
public class AndroidViewViewRule extends BaseView {
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.LinearLayout.groovy b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.LinearLayout.groovy
index 1f1b1c6..7f74bfb 100755
--- a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.LinearLayout.groovy
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.LinearLayout.groovy
@@ -22,8 +22,39 @@ package com.android.adt.gscripts;
public class AndroidWidgetLinearLayoutRule extends BaseLayout {
public static String ATTR_ORIENTATION = "orientation";
+ public static String VALUE_HORIZONTAL = "horizontal";
public static String VALUE_VERTICAL = "vertical";
+ /**
+ * Add an explicit Orientation toggle to the context menu.
+ */
+ public List<MenuAction> getContextMenu(INode selectedNode) {
+
+ def curr_orient = selectedNode.getStringAttr(ANDROID_URI, ATTR_ORIENTATION);
+ if (!curr_orient) {
+ curr_orient = VALUE_VERTICAL;
+ }
+
+ def onChange = { MenuAction.Action action, String valueId, Boolean newValue ->
+ def actionId = action.getId();
+ def node = selectedNode;
+
+ if (actionId == "_orientation") {
+ node.editXml("Change LinearLayout " + ATTR_ORIENTATION) {
+ node.setAttribute(ANDROID_URI, ATTR_ORIENTATION, valueId);
+ }
+ }
+ }
+
+ return super.getContextMenu(selectedNode) +
+ [ new MenuAction.Choices("_orientation", "Orientation",
+ [ horizontal : "Horizontal",
+ vertical : "Vertical" ],
+ curr_orient,
+ onChange ),
+ ];
+ }
+
// ==== Drag'n'drop support ====
DropFeedback onDropEnter(INode targetNode, IDragElement[] elements) {
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.RelativeLayout.groovy b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.RelativeLayout.groovy
index 508d3b4..5d159b5 100755
--- a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.RelativeLayout.groovy
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.RelativeLayout.groovy
@@ -248,7 +248,7 @@ public class AndroidWidgetRelativeLayoutRule extends BaseLayout {
// all the linked id it is referencing.
ids = [];
cachedLinkIds[node] = ids;
- for (attr in node.getAttributes()) {
+ for (attr in node.getLiveAttributes()) {
def attrInfo = node.getAttributeInfo(attr.getUri(), attr.getName());
if (attrInfo == null) {
continue;
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/INode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/INode.java
index 360639c..784b957 100755
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/INode.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/INode.java
@@ -177,6 +177,24 @@ public interface INode {
*/
public IAttributeInfo getAttributeInfo(String uri, String attrName);
+ /**
+ * Returns the list of all attributes declared by this node's descriptor.
+ * <p/>
+ * This returns a fixed list of all attributes known to the view or layout descriptor.
+ * This list does not depend on whether the attributes are actually used in the
+ * XML for this node.
+ * <p/>
+ * Note that for views, the list of attributes also includes the layout attributes
+ * inherited from the parent view. This means callers must not cache this list based
+ * solely on the type of the node, as its attribute list changes depending on the place
+ * of the view in the view hierarchy.
+ * <p/>
+ * If you want attributes actually written in the XML and their values, please use
+ * {@link #getStringAttr(String, String)} or {@link #getLiveAttributes()} instead.
+ *
+ * @return A non-null possible-empty list of {@link IAttributeInfo}.
+ */
+ public IAttributeInfo[] getDeclaredAttributes();
/**
* Returns the list of all attributes defined in the XML for this node.
@@ -185,17 +203,19 @@ public interface INode {
* That means that if called in the context of {@link #editXml(String, Closure)}, the value
* returned here is not affected by {@link #setAttribute(String, String, String)} until
* the editXml closure is completed and the actual XML is updated.
+ * <p/>
+ * If you want a list of all possible attributes, whether used in the XML or not by
+ * this node, please see {@link #getDeclaredAttributes()} instead.
*
* @return A non-null possible-empty list of {@link IAttribute}.
*/
- public IAttribute[] getAttributes();
+ public IAttribute[] getLiveAttributes();
// -----------
/**
- * An XML attribute in an {@link INode}.
+ * An XML attribute in an {@link INode} with a namespace URI, a name and its current value.
* <p/>
- * The attribute is always represented by a namespace URI, a name and a value.
* The name cannot be empty.
* The namespace URI can be empty for an attribute without a namespace but is never null.
* The value can be empty but cannot be null.
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IViewRule.java
index df320ff..3d3764d 100755
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IViewRule.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IViewRule.java
@@ -16,6 +16,7 @@
package com.android.ide.eclipse.adt.editors.layout.gscripts;
+import java.util.List;
import java.util.Map;
@@ -77,6 +78,25 @@ public interface IViewRule {
*/
String getDisplayName();
+ /**
+ * Invoked by the Rules Engine to retrieve a set of actions to customize
+ * the context menu displayed for this view. The result is not cached and the
+ * method is invoked everytime the context menu is about to be shown.
+ * <p/>
+ * Most rules should consider returning <code>super.getContextMenu(node)</code>
+ * and appending their own custom menu actions, if any.
+ * <p/>
+ * Menu actions are either toggles or fixed lists with one currently-selected
+ * item. It is expected that the rule will need to recreate the actions with
+ * different selections when a menu is going to shown, which is why the result
+ * is not cached. However rules are encouraged to cache some or all of the result
+ * to speed up following calls if it makes sense.
+ *
+ * @return Null for no context menu, or a new {@link MenuAction} describing one
+ * or more actions to display in the context menu.
+ */
+ List<MenuAction> getContextMenu(INode node);
+
// ==== Selection ====
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/MenuAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/MenuAction.java
new file mode 100755
index 0000000..3674b02
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/MenuAction.java
@@ -0,0 +1,411 @@
+/*
+ * 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.editors.layout.gscripts;
+
+import groovy.lang.Closure;
+
+import java.util.Map;
+
+/**
+ * A menu action represents one item in the context menu displayed by the GLE canvas.
+ * <p/>
+ * Each action should have a reasonably unique ID. By default actions are stored using
+ * the lexicographical order of the IDs.
+ * Duplicated IDs will be ignored -- that is the first one found will be used.
+ * <p/>
+ * When the canvas has a multiple selection, only actions that are present in <em>all</em>
+ * the selected nodes are shown. Moreover, for a given ID all actions must be equal, for
+ * example they must have the same title and choice but not necessarily the same selection. <br/>
+ * This allows the canvas to only display compatible actions that will work on all selected
+ * elements.
+ * <p/>
+ * Actions can be grouped in sub-menus if necessary. Whether groups (sub-menus) can contain
+ * other groups is implementation dependent. Currently the canvas does not support this, but
+ * we may decide to change this behavior later if deemed useful.
+ * <p/>
+ * All actions and groups are sorted by their ID, using String's natural sorting order.
+ * The only way to change this sorting is by choosing the IDs so they the result end up
+ * sorted as you want it.
+ * <p/>
+ * The {@link MenuAction} is abstract. Users should instantiate either {@link Toggle},
+ * {@link Choices} or {@link Group} instead. These classes are immutable.
+ */
+public abstract class MenuAction {
+
+ /**
+ * The unique id of the action.
+ * @see #getId()
+ */
+ private final String mId;
+ /**
+ * The UI-visible title of the action.
+ */
+ private final String mTitle;
+
+ /**
+ * Creates a new {@link MenuAction} with the given id and the given title.
+ * Actions which have the same id and the same title are deemed equivalent.
+ *
+ * @param id The unique id of the action, which must be similar for all actions that
+ * perform the same task. Cannot be null.
+ * @param title The UI-visible title of the action.
+ */
+ private MenuAction(String id, String title) {
+ mId = id;
+ mTitle = title;
+ }
+
+ /**
+ * Returns the unique id of the action. In the context of a multiple selection,
+ * actions which have the same id are collapsed together and must represent the same
+ * action. Cannot be null.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the UI-visible title of the action, shown in the context menu.
+ * Cannot be null.
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Actions which have the same id and the same title are deemed equivalent.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof MenuAction) {
+ MenuAction rhs = (MenuAction) obj;
+
+ if (mId != rhs.mId && !(mId != null && mId.equals(rhs.mId))) return false;
+ if (mTitle != rhs.mTitle &&
+ !(mTitle != null && mTitle.equals(rhs.mTitle))) return false;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Actions which have the same id and the same title have the same hash code.
+ */
+ @Override
+ public int hashCode() {
+ int h = mId == null ? 0 : mId.hashCode();
+ h = h ^ (mTitle == null ? 0 : mTitle.hashCode());
+ return h;
+ }
+
+ /**
+ * A group of actions, displayed in a sub-menu.
+ * <p/>
+ * Note that group can be seen as a "group declaration": the group does not hold a list
+ * actions that it will contain. This merely let the canvas create a sub-menu with the
+ * given title and actions that define this group-id will be placed in the sub-menu.
+ * <p/>
+ * The current canvas has the following implementation details: <br/>
+ * - There's only one level of sub-menu.
+ * That is you can't have a sub-menu inside another sub-menu.
+ * This is expressed by the fact that groups do not have a parent group-id. <br/>
+ * - It is not currently necessary to define a group before defining actions that refer
+ * to that group. Moreover, in the context of a multiple selection, one view could
+ * contribute actions to any group even if created by another view. Both practices
+ * are discouraged. <br/>
+ * - Actions which group-id do not match any known group will simply be placed in the
+ * root context menu. <br/>
+ * - Empty groups do not create visible sub-menus. <br/>
+ * These implementations details may change in the future and should not be relied upon.
+ */
+ public static class Group extends MenuAction {
+
+ /**
+ * Constructs a new group of actions.
+ *
+ * @param id The id of the group. Must be unique. Cannot be null.
+ * @param title The UI-visible title of the group, shown in the sub-menu.
+ */
+ public Group(String id, String title) {
+ super(id, title);
+ }
+ }
+
+ /**
+ * The base class for {@link Toggle} and {@link Choices}.
+ */
+ public static abstract class Action extends MenuAction {
+
+
+
+ /**
+ * A closure executed when the action is selected in the context menu.
+ *
+ * @see #getAction() for details on the closure parameters.
+ */
+ private final Closure mAction;
+ /**
+ * An optional group id, to place the action in a given sub-menu.
+ * @null This value can be null.
+ */
+ private final String mGroupId;
+
+ /**
+ * Constructs a new base {@link MenuAction} with its ID, title and action closure.
+ *
+ * @param id The unique ID of the action. Must not be null.
+ * @param title The title of the action. Must not be null.
+ * @param groupId The optional group id, to place the action in a given sub-menu.
+ * Can be null.
+ * @param action The closure executed when the action is selected. Must not be null.
+ * See {@link #getAction()} for the closure parameters.
+ */
+ private Action(String id, String title, String groupId, Closure action) {
+ super(id, title);
+ mGroupId = groupId;
+ mAction = action;
+ }
+
+ /**
+ * Returns the closure executed when the action is selected in the context menu.
+ * Cannot be null.
+ * <p/>
+ * The closure takes the following parameters: <br/>
+ * - {@link MenuAction} <code>action</code>: the MenuAction being applied. <br/>
+ * - {@link String} <code>valueId</code>: for a Choices action, the string id of the
+ * selected choice. <br/>
+ * - {@link Boolean} <code>newValue</code>: for a toggle or for a flag, true if
+ * the item is being checked, false if being unchecked. For enums this is not
+ * useful; however for flags it allows one to add or remove items to the flag's
+ * choices.
+ */
+ public Closure getAction() {
+ return mAction;
+ }
+
+ /**
+ * Returns the optional id of an existing group or null
+ * @null This value can be null.
+ */
+ public String getGroupId() {
+ return mGroupId;
+ }
+
+ /**
+ * Two actions are equal if the have the same id, title and group-id.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Action && super.equals(obj)) {
+ Action rhs = (Action) obj;
+ return mGroupId == rhs.mGroupId ||
+ (mGroupId != null && mGroupId.equals(rhs.mGroupId));
+ }
+ return false;
+ }
+
+ /**
+ * Two actions have the same hash code if the have the same id, title and group-id.
+ */
+ @Override
+ public int hashCode() {
+ int h = super.hashCode();
+ h = h ^ (mGroupId == null ? 0 : mGroupId.hashCode());
+ return h;
+ }
+ }
+
+ /**
+ * A toggle is a simple on/off action, displayed as an item in a context menu
+ * with a check mark if the item is checked.
+ */
+ public static class Toggle extends Action {
+ /**
+ * True if the item is displayed with a check mark.
+ */
+ final private boolean mIsChecked;
+
+ /**
+ * Creates a new immutable toggle action.
+ * This action has no group-id and will show up in the root of the context menu.
+ *
+ * @param id The unique id of the action. Cannot be null.
+ * @param title The UI-visible title of the context menu item. Cannot be null.
+ * @param isChecked Whether the context menu item has a check mark.
+ * @param action A closure to execute when the context menu item is selected.
+ */
+ public Toggle(String id, String title, boolean isChecked, Closure action) {
+ this(id, title, isChecked, null /*group-id*/, action);
+ }
+
+ /**
+ * Creates a new immutable toggle action.
+ *
+ * @param id The unique id of the action. Cannot be null.
+ * @param title The UI-visible title of the context menu item. Cannot be null.
+ * @param isChecked Whether the context menu item has a check mark.
+ * @param groupId The optional group id, to place the action in a given sub-menu.
+ * Can be null.
+ * @param action A closure to execute when the context menu item is selected.
+ */
+ public Toggle(String id, String title, boolean isChecked, String groupId, Closure action) {
+ super(id, title, groupId, action);
+ mIsChecked = isChecked;
+ }
+
+ /**
+ * Returns true if the item is displayed with a check mark.
+ */
+ public boolean isChecked() {
+ return mIsChecked;
+ }
+
+ /**
+ * Two toggles are equal if they have the same id, title and group-id.
+ * It is acceptable for the checked state and action closure to be different.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ return super.equals(obj);
+ }
+
+ /**
+ * Two toggles have the same hash code if they have the same id, title and group-id.
+ */
+ @Override
+ public int hashCode() {
+ return super.hashCode();
+ }
+ }
+
+ /**
+ * A "choices" is a one-out-of-many-choices action, displayed as a sub-menu with one or more
+ * items, with either zero or more of them being checked.
+ * <p/>
+ * Implementation detail: empty choices will not be displayed in the context menu.
+ * <p/>
+ * Choice items are sorted by their id, using String's natural sorting order.
+ */
+ public static class Choices extends Action {
+
+ /**
+ * Special value which will insert a separator in the choices' submenu.
+ */
+ public final static String SEPARATOR = "----";
+ /**
+ * Character used to split multiple checked choices, see {@link #getCurrent()}.
+ * The pipe character "|" is used, to natively match Android resource flag separators.
+ */
+ public final static String CHOICE_SEP = "|";
+
+ /**
+ * A non-null map of id=>choice-title. The map could be empty but not null.
+ */
+ private final Map<String, String> mChoices;
+ /**
+ * One or more id for the checked choice(s) that will be check marked.
+ * Can be null. Can be an id not present in the choices map.
+ * If more than one choice, they must be separated by {@link #CHOICE_SEP}.
+ */
+ private final String mCurrent;
+
+ /**
+ * Creates a new immutable multiple-choice action.
+ * This action has no group-id and will show up in the root of the context menu.
+ *
+ * @param id The unique id of the action. Cannot be null.
+ * @param title The UI-visible title of the context menu item. Cannot be null.
+ * @param choices A map id=>title for all the multiple-choice items. Cannot be null.
+ * @param current The id(s) of the current choice(s) that will be check marked.
+ * Can be null. Can be an id not present in the choices map.
+ * There can be more than one id separated by {@link #CHOICE_SEP}.
+ * @param action A closure to execute when the context menu item is selected.
+ */
+ public Choices(String id, String title,
+ Map<String, String> choices,
+ String current,
+ Closure action) {
+ this(id, title, choices, current, null /*group-id*/, action);
+ }
+
+ /**
+ * Creates a new immutable multiple-choice action.
+ *
+ * @param id The unique id of the action. Cannot be null.
+ * @param title The UI-visible title of the context menu item. Cannot be null.
+ * @param choices A map id=>title for all the multiple-choice items. Cannot be null.
+ * @param current The id(s) of the current choice(s) that will be check marked.
+ * Can be null. Can be an id not present in the choices map.
+ * There can be more than one id separated by {@link #CHOICE_SEP}.
+ * @param groupId The optional group id, to place the action in a given sub-menu.
+ * Can be null.
+ * @param action A closure to execute when the context menu item is selected.
+ */
+ public Choices(String id, String title,
+ Map<String, String> choices,
+ String current,
+ String groupId,
+ Closure action) {
+ super(id, title, groupId, action);
+ mChoices = choices;
+ mCurrent = current;
+ }
+
+ /**
+ * Return the map of id=>choice-title. The map could be empty but not null.
+ */
+ public Map<String, String> getChoices() {
+ return mChoices;
+ }
+
+ /**
+ * Returns the id(s) of the current choice(s) that are check marked.
+ * Can be null. Can be an id not present in the choices map.
+ * There can be more than one id separated by {@link #CHOICE_SEP}.
+ */
+ public String getCurrent() {
+ return mCurrent;
+ }
+
+ /**
+ * Two multiple choices are equal if they have the same id, title, group-id and choices.
+ * It is acceptable for the current state and action closure to be different.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Choices && super.equals(obj)) {
+ Choices rhs = (Choices) obj;
+ return mChoices.equals(rhs.mChoices);
+ }
+ return false;
+ }
+
+ /**
+ * Two multiple choices have the same hash code if they have the same id, title,
+ * group-id and choices.
+ */
+ @Override
+ public int hashCode() {
+ int h = super.hashCode();
+
+ if (mChoices != null) {
+ h = h ^ mChoices.hashCode();
+ }
+ return h;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java
index 0e56bba..b22999e 100755
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java
@@ -19,6 +19,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.editors.layout.gscripts.IDragElement;
import com.android.ide.eclipse.adt.editors.layout.gscripts.INode;
+import com.android.ide.eclipse.adt.editors.layout.gscripts.MenuAction;
import com.android.ide.eclipse.adt.editors.layout.gscripts.Point;
import com.android.ide.eclipse.adt.editors.layout.gscripts.Rect;
import com.android.ide.eclipse.adt.editors.layout.gscripts.IDragElement.IDragAttribute;
@@ -44,6 +45,7 @@ import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ActionContributionItem;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IContributionItem;
+import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
@@ -89,6 +91,7 @@ import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.actions.ActionFactory;
@@ -104,16 +107,24 @@ import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.xml.core.internal.document.NodeContainer;
import org.w3c.dom.Node;
+import groovy.lang.Closure;
+
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.awt.image.Raster;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
+import java.util.Map;
import java.util.Set;
+import java.util.TreeMap;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
/**
* Displays the image rendered by the {@link GraphicalEditorPart} and handles
@@ -135,7 +146,8 @@ import java.util.Set;
/*
* TODO list:
* - gray on error, keep select but disable d'n'd.
- * - context menu handling of layout + local props (via IViewRules)
+ * - context menu: enum clear, flag values, toggles as tri-states
+ * - context menu: impl custom layout width/height
* - properly handle custom views
*/
class LayoutCanvas extends Canvas implements ISelectionProvider {
@@ -1654,64 +1666,6 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
//---------------
- private void createContextMenu() {
-
- mMenuManager = new MenuManager();
- createMenuAction(mMenuManager);
- setMenu(mMenuManager.createContextMenu(this));
-
- /*
- TODO insert generated menus/actions here.
-
- Menu menu = new Menu(this);
-
- ISharedImages wbImages = PlatformUI.getWorkbench().getSharedImages();
-
- final MenuItem cutItem = new MenuItem (menu, SWT.PUSH);
- cutItem.setText("Cut");
- cutItem.setImage(wbImages.getImage(ISharedImages.IMG_TOOL_CUT));
- cutItem.addListener (SWT.Selection, new Listener () {
- public void handleEvent (Event event) {
- // TODO
- }
- });
-
- menu.addMenuListener(new MenuAdapter() {
- @Override
- public void menuShown(MenuEvent e) {
- cutItem.setEnabled(!mSelections.isEmpty());
- super.menuShown(e);
- }
- });
-
- setMenu(menu);
- */
-
- }
-
- /** Update menu actions that depends on the selection. */
- private void updateMenuActions() {
-
- boolean hasSelection = !mSelections.isEmpty();
-
- mCutAction.setEnabled(hasSelection);
- mCopyAction.setEnabled(hasSelection);
- mDeleteAction.setEnabled(hasSelection);
- mSelectAllAction.setEnabled(hasSelection);
-
- // The paste operation is only available if we can paste our custom type.
- // We do not currently support pasting random text (e.g. XML). Maybe later.
- SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
- boolean hasSxt = false;
- for (TransferData td : mClipboard.getAvailableTypes()) {
- if (sxt.isSupportedType(td)) {
- hasSxt = true;
- break;
- }
- }
- mPasteAction.setEnabled(hasSxt);
- }
-
/**
* Invoked by the constructor to add our cut/copy/paste/delete/select-all
* handlers in the global action handlers of this editor's site.
@@ -1779,6 +1733,29 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
copyActionAttributes(mSelectAllAction, ActionFactory.SELECT_ALL);
}
+ /** Update menu actions that depends on the selection. */
+ private void updateMenuActions() {
+
+ boolean hasSelection = !mSelections.isEmpty();
+
+ mCutAction.setEnabled(hasSelection);
+ mCopyAction.setEnabled(hasSelection);
+ mDeleteAction.setEnabled(hasSelection);
+ mSelectAllAction.setEnabled(hasSelection);
+
+ // The paste operation is only available if we can paste our custom type.
+ // We do not currently support pasting random text (e.g. XML). Maybe later.
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+ boolean hasSxt = false;
+ for (TransferData td : mClipboard.getAvailableTypes()) {
+ if (sxt.isSupportedType(td)) {
+ hasSxt = true;
+ break;
+ }
+ }
+ mPasteAction.setEnabled(hasSxt);
+ }
+
/**
* Helper for {@link #setupGlobalActionHandlers()}.
* Copies the action attributes form the given {@link ActionFactory}'s action to
@@ -1803,6 +1780,42 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
action.setDisabledImageDescriptor(wa.getDisabledImageDescriptor());
}
+ private void createContextMenu() {
+ // Create the menu manager and fill it with the static actions.
+ mMenuManager = new MenuManager() {
+ @Override
+ public boolean isDynamic() {
+ return true;
+ }
+ };
+
+ createMenuAction(mMenuManager);
+ Menu menu = mMenuManager.createContextMenu(this);
+ setMenu(menu);
+
+ // Remember how many static actions we have. Then each time the menu is
+ // shown, find dynamic contributions based on the current selection and insert
+ // them at the beginning of the menu.
+ final int numStaticActions = mMenuManager.getSize();
+ mMenuManager.addMenuListener(new IMenuListener() {
+ public void menuAboutToShow(IMenuManager manager) {
+
+ // Remove any previous dynamic contributions to keep only the
+ // default static items.
+ int n = mMenuManager.getSize() - numStaticActions;
+ if (n > 0) {
+ IContributionItem[] items = mMenuManager.getItems();
+ for (int i = 0; i < n; i++) {
+ mMenuManager.remove(items[i]);
+ }
+ }
+
+ populateDynamicContextMenu();
+ }
+ });
+
+ }
+
/**
* Invoked by the constructor to create our *static* context menu.
* <p/>
@@ -1825,18 +1838,247 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
manager.add(mDeleteAction);
manager.add(mSelectAllAction);
- // TODO add view-sensitive menu items.
-
manager.add(new Separator());
String showInLabel = IDEWorkbenchMessages.Workbench_showIn;
MenuManager showInSubMenu= new MenuManager(showInLabel);
- showInSubMenu.add(
- ContributionItemFactory.VIEWS_SHOW_IN.create(
- mLayoutEditor.getSite().getWorkbenchWindow()));
+ showInSubMenu.add(ContributionItemFactory.VIEWS_SHOW_IN.create(
+ mLayoutEditor.getSite().getWorkbenchWindow()));
manager.add(showInSubMenu);
}
+
+ private void populateDynamicContextMenu() {
+ // Collect actions for current selection
+
+ // Map action-id => action object (one per selected view that defined it)
+ final TreeMap<String /*id*/, ArrayList<MenuAction>> actionsMap =
+ new TreeMap<String, ArrayList<MenuAction>>();
+
+ // Map group-id => actions to place in this group.
+ TreeMap<String /*id*/, MenuAction.Group> groupMap = new TreeMap<String, MenuAction.Group>();
+
+ int maxMenuSelection = 0;
+ for (CanvasSelection selection : mSelections) {
+ List<MenuAction> viewActions = null;
+ if (selection != null) {
+ CanvasViewInfo vi = selection.getViewInfo();
+ if (vi != null) {
+ viewActions = getMenuActions(vi);
+ }
+ }
+ if (viewActions == null) {
+ continue;
+ }
+
+ boolean foundAction = false;
+ for (MenuAction action : viewActions) {
+ if (action.getId() == null || action.getTitle() == null) {
+ // TODO invalid action. Log verbose error.
+ continue;
+ }
+
+ String id = action.getId();
+
+ if (action instanceof MenuAction.Group) {
+ if (!groupMap.containsKey(id)) {
+ groupMap.put(id, (MenuAction.Group) action);
+ }
+ continue;
+ }
+
+ ArrayList<MenuAction> actions = actionsMap.get(id);
+ if (actions == null) {
+ actions = new ArrayList<MenuAction>();
+ actionsMap.put(id, actions);
+ }
+
+ // All the actions for the same id should have be equal
+ if (!actions.isEmpty()) {
+ if (action.equals(actions.get(0))) {
+ // TODO invalid type mismatch. Log verbose error.
+ continue;
+ }
+ }
+
+ actions.add(action);
+ foundAction = true;
+ }
+
+ if (foundAction) {
+ maxMenuSelection++;
+ }
+ }
+
+ // Now create the actual menu contributions
+ String endId = mMenuManager.getItems()[0].getId();
+
+ Separator sep = new Separator();
+ sep.setId("-dyn-gle-sep"); //$NON-NLS-1$
+ mMenuManager.insertBefore(endId, sep);
+ endId = sep.getId();
+
+ // First create the groups
+ Map<String, MenuManager> menuGroups = new HashMap<String, MenuManager>();
+ for (MenuAction.Group group : groupMap.values()) {
+ String id = group.getId();
+ MenuManager submenu = new MenuManager(group.getTitle(), id);
+ menuGroups.put(id, submenu);
+ mMenuManager.insertBefore(endId, submenu);
+ endId = id;
+ }
+
+ boolean needGroupSep = !menuGroups.isEmpty();
+
+ // Now fill in the actions
+ for (ArrayList<MenuAction> actions : actionsMap.values()) {
+ // Filter actions... if we have a multiple selection, only accept actions
+ // which are common to *all* the selection which actually returned at least
+ // one menu action.
+ if (actions == null ||
+ actions.isEmpty() ||
+ actions.size() != maxMenuSelection) {
+ continue;
+ }
+
+ if (!(actions.get(0) instanceof MenuAction.Action)) {
+ continue;
+ }
+
+ final MenuAction.Action action = (MenuAction.Action) actions.get(0);
+
+ IContributionItem contrib = null;
+
+ if (action instanceof MenuAction.Toggle) {
+
+ final boolean isChecked = ((MenuAction.Toggle) action).isChecked();
+ Action a = new Action(action.getTitle(), IAction.AS_CHECK_BOX) {
+ @Override
+ public void run() {
+ // Invoke the closures of all the actions using the same action-id
+ for (MenuAction a2 : actionsMap.get(action.getId())) {
+ if (a2 instanceof MenuAction.Action) {
+ Closure c = ((MenuAction.Action) a2).getAction();
+ if (c != null) {
+ mRulesEngine.callClosure(
+ ((MenuAction.Action) a2).getAction(),
+ // Closure parameters are action, valueId, newValue
+ action,
+ null, // no valueId for a toggle
+ !isChecked);
+ }
+ }
+ }
+ }
+ };
+ a.setId(action.getId());
+ a.setChecked(isChecked);
+
+ contrib = new ActionContributionItem(a);
+
+ } else if (action instanceof MenuAction.Choices) {
+
+ Map<String, String> choiceMap = ((MenuAction.Choices) action).getChoices();
+ if (choiceMap != null && !choiceMap.isEmpty()) {
+ MenuManager submenu = new MenuManager(action.getTitle(), action.getId());
+
+ // Convert to a tree map as needed so that keys be naturally ordered.
+ if (!(choiceMap instanceof TreeMap<?, ?>)) {
+ choiceMap = new TreeMap<String, String>(choiceMap);
+ }
+
+ String current = ((MenuAction.Choices) action).getCurrent();
+ Set<String> currents = null;
+ if (current.indexOf(MenuAction.Choices.CHOICE_SEP) >= 0) {
+ currents = new HashSet<String>(
+ Arrays.asList(current.split(
+ Pattern.quote(MenuAction.Choices.CHOICE_SEP))));
+ current = null;
+ }
+
+ for (Entry<String, String> entry : choiceMap.entrySet() ) {
+ final String key = entry.getKey();
+ String title = entry.getValue();
+
+ if (key == null || title == null) {
+ continue;
+ }
+
+ if (MenuAction.Choices.SEPARATOR.equals(title)) {
+ submenu.add(new Separator());
+ continue;
+ }
+
+ final boolean isChecked =
+ (currents != null && currents.contains(key)) ||
+ key.equals(current);
+
+ Action a = new Action(title, IAction.AS_CHECK_BOX) {
+ @Override
+ public void run() {
+ // Invoke the closures of all the actions using the same action-id
+ for (MenuAction a2 : actionsMap.get(action.getId())) {
+ if (a2 instanceof MenuAction.Action) {
+ mRulesEngine.callClosure(
+ ((MenuAction.Action) a2).getAction(),
+ // Closure parameters are action, valueId, newValue
+ action,
+ key,
+ !isChecked);
+ }
+ }
+ }
+ };
+ a.setId(String.format("%s_%s", action.getId(), key)); //$NON-NLS-1$
+ a.setChecked(isChecked);
+ submenu.add(a);
+ }
+
+ contrib = submenu;
+ }
+ }
+
+ if (contrib != null) {
+ MenuManager groupMenu = menuGroups.get(action.getGroupId());
+ if (groupMenu != null) {
+ groupMenu.add(contrib);
+ } else {
+ if (needGroupSep) {
+ needGroupSep = false;
+
+ sep = new Separator();
+ sep.setId("-dyn-gle-sep2"); //$NON-NLS-1$
+ mMenuManager.insertBefore(endId, sep);
+ endId = sep.getId();
+ }
+ mMenuManager.insertBefore(endId, contrib);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the menu actions computed by the groovy rule associated with this view.
+ */
+ public List<MenuAction> getMenuActions(CanvasViewInfo vi) {
+ if (vi == null) {
+ return null;
+ }
+
+ NodeProxy node = mNodeFactory.create(vi);
+ if (node == null) {
+ return null;
+ }
+
+ List<MenuAction> actions = mRulesEngine.callGetContextMenu(node);
+ if (actions == null || actions.size() == 0) {
+ return null;
+ }
+
+ return actions;
+ }
+
+
/**
* Invoked by {@link #mSelectAllAction}. It clears the selection and then
* selects everything (all views and all their children).
@@ -2182,4 +2424,5 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
private void debugPrintf(String message, Object... params) {
if (DEBUG) AdtPlugin.printToConsole("Canvas", String.format(message, params));
}
+
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java
index 739cd47..8dc81a6 100755
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java
@@ -16,8 +16,6 @@
package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
-import com.android.ide.eclipse.adt.annotations.VisibleForTesting;
-import com.android.ide.eclipse.adt.annotations.VisibleForTesting.Visibility;
import com.android.ide.eclipse.adt.editors.layout.gscripts.IDragElement.IDragAttribute;
import com.android.ide.eclipse.adt.editors.layout.gscripts.INode.IAttribute;
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 16b50b4..562c89e 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
@@ -31,6 +31,7 @@ import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElement
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
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.resources.AttributeInfo;
import org.eclipse.swt.graphics.Rectangle;
import org.w3c.dom.NamedNodeMap;
@@ -317,7 +318,20 @@ public class NodeProxy implements INode {
return null;
}
- public IAttribute[] getAttributes() {
+ public IAttributeInfo[] getDeclaredAttributes() {
+
+ AttributeDescriptor[] descs = mNode.getAttributeDescriptors();
+ int n = descs.length;
+ IAttributeInfo[] infos = new AttributeInfo[n];
+
+ for (int i = 0; i < n; i++) {
+ infos[i] = descs[i].getAttributeInfo();
+ }
+
+ return infos;
+ }
+
+ public IAttribute[] getLiveAttributes() {
UiElementNode uiNode = mNode;
if (uiNode.getXmlNode() != null) {
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 2fcff2b..7790411 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
@@ -26,6 +26,7 @@ import com.android.ide.eclipse.adt.editors.layout.gscripts.IDragElement;
import com.android.ide.eclipse.adt.editors.layout.gscripts.IGraphics;
import com.android.ide.eclipse.adt.editors.layout.gscripts.INode;
import com.android.ide.eclipse.adt.editors.layout.gscripts.IViewRule;
+import com.android.ide.eclipse.adt.editors.layout.gscripts.MenuAction;
import com.android.ide.eclipse.adt.editors.layout.gscripts.Point;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
@@ -49,6 +50,7 @@ import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
+import groovy.lang.Closure;
import groovy.lang.ExpandoMetaClass;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyCodeSource;
@@ -64,6 +66,7 @@ import java.nio.charset.Charset;
import java.security.CodeSource;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
/* TODO:
@@ -158,6 +161,30 @@ public class RulesEngine {
}
/**
+ * A convenience function that invokes {@link Closure#call(Object[])} with the
+ * parameters given to the method.
+ * Any exception thrown by the closure invocation is trapped and logged.
+ *
+ * @param closure The closure to invoke
+ * @param params The parameters for the closure, conveniently wrapped as an
+ * <code>Object[]</code>
+ * @return The optional return value from the closure call or null.
+ */
+ public Object callClosure(Closure closure, Object...params) {
+ try {
+ return closure.call(params);
+
+ } catch (Exception e) {
+ logError("invokeClosure %s failed: %s",
+ closure.getClass().getSimpleName(),
+ e.toString());
+ }
+
+ return null;
+ }
+
+
+ /**
* Invokes {@link IViewRule#getDisplayName()} on the rule matching the specified element.
*
* @param element The view element to target. Can be null.
@@ -183,6 +210,31 @@ public class RulesEngine {
}
/**
+ * Invokes {@link IViewRule#getContextMenu(INode)} on the rule matching the specified element.
+ *
+ * @param selectedNode The node selected. Never null.
+ * @return Null if the rule failed, there's no rule or the rule does not provide
+ * any custom menu actions. Otherwise, a list of {@link MenuAction}.
+ */
+ public List<MenuAction> callGetContextMenu(NodeProxy selectedNode) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(selectedNode.getNode());
+
+ if (rule != null) {
+ try {
+ return rule.getContextMenu(selectedNode);
+
+ } catch (Exception e) {
+ logError("%s.getContextMenu() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ /**
* Invokes {@link IViewRule#onSelected(IGraphics, INode, String, boolean)}
* on the rule matching the specified element.
*
@@ -764,5 +816,4 @@ public class RulesEngine {
}
}
-
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java
index 38e2527..dc8b79c 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java
@@ -36,6 +36,7 @@ import java.util.List;
*/
public class UiViewElementNode extends UiElementNode {
+ /** An AttributeDescriptor array that depends on the current UiParent. */
private AttributeDescriptor[] mCachedAttributeDescriptors;
public UiViewElementNode(ViewElementDescriptor elementDescriptor) {