diff options
author | Raphael Moll <ralf@android.com> | 2010-09-01 16:34:51 -0700 |
---|---|---|
committer | Android Code Review <code-review@android.com> | 2010-09-01 16:34:51 -0700 |
commit | 7e67b9023d75320615d8a1f8472b9d0d0171b6be (patch) | |
tree | 171ccd421e336a2e24f5746355a169c3c9c071a0 /eclipse/plugins | |
parent | f0dad1edb62d1e56848113ec1164cb76ac0edc12 (diff) | |
parent | 44a39167e3f9ee89e495716b7c3f0094ab91f2b1 (diff) | |
download | sdk-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')
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) { |